16
16
17
17
package androidx .test .espresso .base ;
18
18
19
+ import android .content .Context ;
20
+ import android .hardware .input .InputManager ;
19
21
import android .os .Build ;
20
22
import android .os .SystemClock ;
21
23
import android .util .Log ;
22
24
import android .view .InputDevice ;
23
25
import android .view .InputEvent ;
24
26
import android .view .KeyEvent ;
25
27
import android .view .MotionEvent ;
28
+ import android .view .View ;
26
29
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 ;
30
36
31
37
/**
32
38
* 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.
34
40
*/
35
41
final class InputManagerEventInjectionStrategy implements EventInjectionStrategy {
36
42
private static final String TAG = "EventInjectionStrategy" ;
37
43
// The delay time to allow the soft keyboard to dismiss.
38
44
private static final long KEYBOARD_DISMISSAL_DELAY_MILLIS = 1000L ;
39
45
40
46
// 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 ;
47
68
48
69
InputManagerEventInjectionStrategy () {}
49
70
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
-
106
71
@ Override
107
72
public boolean injectKeyEvent (KeyEvent keyEvent ) throws InjectEventSecurityException {
108
73
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 ();
115
79
if (cause instanceof SecurityException ) {
116
80
throw new InjectEventSecurityException (cause );
117
81
}
118
- throw new RuntimeException (e );
82
+ throw new RuntimeException (cause );
119
83
} catch (SecurityException e ) {
120
84
throw new InjectEventSecurityException (e );
121
85
}
@@ -135,18 +99,14 @@ private boolean innerInjectMotionEvent(MotionEvent motionEvent, boolean shouldRe
135
99
// TODO: proper handling of events from a trackball (SOURCE_TRACKBALL) and joystick.
136
100
if ((motionEvent .getSource () & InputDevice .SOURCE_CLASS_POINTER ) == 0
137
101
&& !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 );
140
104
}
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 ();
150
110
if (cause instanceof SecurityException ) {
151
111
if (shouldRetry ) {
152
112
Log .w (
@@ -164,7 +124,7 @@ private boolean innerInjectMotionEvent(MotionEvent motionEvent, boolean shouldRe
164
124
cause );
165
125
}
166
126
} else {
167
- throw new RuntimeException (e );
127
+ throw new RuntimeException (e . getCause () );
168
128
}
169
129
} catch (SecurityException e ) {
170
130
throw new InjectEventSecurityException (e );
@@ -179,4 +139,32 @@ private static boolean isFromTouchpadInGlassDevice(MotionEvent motionEvent) {
179
139
|| Build .DEVICE .contains ("wingman" ))
180
140
&& ((motionEvent .getSource () & InputDevice .SOURCE_TOUCHPAD ) != 0 );
181
141
}
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
+ }
182
170
}
0 commit comments