diff --git a/espresso/CHANGELOG.md b/espresso/CHANGELOG.md index a65d27c91..e260cbc72 100644 --- a/espresso/CHANGELOG.md +++ b/espresso/CHANGELOG.md @@ -18,6 +18,7 @@ The following artifacts were released: * Fix #2349, where multi-process + different rotation on 2 activities would instantly timeout when waiting for the UI to rotate. +* Use getSystemService instead of reflective InputManager.getInstance **New Features** diff --git a/espresso/core/java/androidx/test/espresso/base/BaseLayerModule.java b/espresso/core/java/androidx/test/espresso/base/BaseLayerModule.java index 67ae0ce92..011b9d63d 100644 --- a/espresso/core/java/androidx/test/espresso/base/BaseLayerModule.java +++ b/espresso/core/java/androidx/test/espresso/base/BaseLayerModule.java @@ -120,12 +120,12 @@ public ActiveRootLister provideActiveRootLister(RootsOracle rootsOracle) { @Provides @Singleton public EventInjector provideEventInjector() { - // Adroid uses input manager to inject events. + // Android uses input manager to inject events. // Instrumentation does not check if the event presses went through by checking the // boolean return value of injectInputEvent, which is why we created this class to better // handle lost/dropped press events. Instrumentation cannot be used as a fallback strategy, // since this will be executed on the main thread. - return new EventInjector(new InputManagerEventInjectionStrategy().initialize()); + return new EventInjector(new InputManagerEventInjectionStrategy()); } /** Holder for AtomicReference which allows updating it at runtime. */ diff --git a/espresso/core/java/androidx/test/espresso/base/InputManagerEventInjectionStrategy.java b/espresso/core/java/androidx/test/espresso/base/InputManagerEventInjectionStrategy.java index 62187d7b3..85a937dbb 100644 --- a/espresso/core/java/androidx/test/espresso/base/InputManagerEventInjectionStrategy.java +++ b/espresso/core/java/androidx/test/espresso/base/InputManagerEventInjectionStrategy.java @@ -16,6 +16,8 @@ package androidx.test.espresso.base; +import android.content.Context; +import android.hardware.input.InputManager; import android.os.Build; import android.os.SystemClock; import android.util.Log; @@ -23,14 +25,18 @@ import android.view.InputEvent; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.View; import androidx.test.espresso.InjectEventSecurityException; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import androidx.test.internal.platform.reflect.ReflectionException; +import androidx.test.internal.platform.reflect.ReflectiveMethod; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.platform.view.inspector.WindowInspectorCompat; +import androidx.test.platform.view.inspector.WindowInspectorCompat.ViewRetrievalException; +import java.util.List; /** * An {@link EventInjectionStrategy} that uses the input manager to inject Events. This strategy - * supports API level 16 and above. + * supports API level 23 and above. */ final class InputManagerEventInjectionStrategy implements EventInjectionStrategy { private static final String TAG = "EventInjectionStrategy"; @@ -38,84 +44,42 @@ final class InputManagerEventInjectionStrategy implements EventInjectionStrategy private static final long KEYBOARD_DISMISSAL_DELAY_MILLIS = 1000L; // Used in reflection - private boolean initComplete; - private Method injectInputEventMethod; - private Method setSourceMotionMethod; - private Object instanceInputManagerObject; - private int asyncEventMode; - private int syncEventMode; + // TODO(b/404661556): use a public API method instead + private final ReflectiveMethod injectInputEventMethod = + new ReflectiveMethod<>( + InputManager.class, "injectInputEvent", InputEvent.class, Integer.TYPE); + + // only used on APIs < 23 + private final ReflectiveMethod getInstanceMethod = + new ReflectiveMethod<>(InputManager.class, "getInstance"); + ; + + // hardcoded copies of private InputManager fields. + // historically these were obtained via reflection, but that seems + // wasteful as these values have not changed since they were introduced + // copy of private InputManager.INJECT_INPUT_EVENT_MODE_ASYNC. + // This value has always been 0 + private static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0; + + // Setting event mode to INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH to ensure + // that we've dispatched the event and any side effects its had on the view hierarchy + // have occurred. + private static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; InputManagerEventInjectionStrategy() {} - InputManagerEventInjectionStrategy initialize() { - if (initComplete) { - return this; - } - - try { - Log.d(TAG, "Creating injection strategy with input manager."); - - // Get the InputManager class object and initialize if necessary. - Class inputManagerClassObject = Class.forName("android.hardware.input.InputManager"); - Method getInstanceMethod = inputManagerClassObject.getDeclaredMethod("getInstance"); - getInstanceMethod.setAccessible(true); - - instanceInputManagerObject = getInstanceMethod.invoke(inputManagerClassObject); - - injectInputEventMethod = - instanceInputManagerObject - .getClass() - .getDeclaredMethod("injectInputEvent", InputEvent.class, Integer.TYPE); - injectInputEventMethod.setAccessible(true); - - // Setting event mode to INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH to ensure - // that we've dispatched the event and any side effects its had on the view hierarchy - // have occurred. - Field motionEventModeField = - inputManagerClassObject.getField("INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH"); - motionEventModeField.setAccessible(true); - syncEventMode = motionEventModeField.getInt(inputManagerClassObject); - - if (Build.VERSION.SDK_INT >= 28) { - // Starting from android P it is not allowed to access this field with reflection, hardcoded - // this value as workaround. - asyncEventMode = 0; - } else { - Field asyncMotionEventModeField = - inputManagerClassObject.getField("INJECT_INPUT_EVENT_MODE_ASYNC"); - asyncMotionEventModeField.setAccessible(true); - asyncEventMode = asyncMotionEventModeField.getInt(inputManagerClassObject); - } - - setSourceMotionMethod = MotionEvent.class.getDeclaredMethod("setSource", Integer.TYPE); - initComplete = true; - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - return this; - } - @Override public boolean injectKeyEvent(KeyEvent keyEvent) throws InjectEventSecurityException { try { - return (Boolean) - injectInputEventMethod.invoke(instanceInputManagerObject, keyEvent, syncEventMode); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); + return injectInputEventMethod.invoke( + getInputManager(), keyEvent, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); + } catch (ReflectionException e) { + // annoyingly, ReflectiveMethod always rewraps the underlying exception + Throwable cause = e.getCause().getCause(); if (cause instanceof SecurityException) { throw new InjectEventSecurityException(cause); } - throw new RuntimeException(e); + throw new RuntimeException(cause); } catch (SecurityException e) { throw new InjectEventSecurityException(e); } @@ -135,18 +99,14 @@ private boolean innerInjectMotionEvent(MotionEvent motionEvent, boolean shouldRe // TODO: proper handling of events from a trackball (SOURCE_TRACKBALL) and joystick. if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0 && !isFromTouchpadInGlassDevice(motionEvent)) { - // Need to do runtime invocation of setSource because it was not added until 2.3_r1. - setSourceMotionMethod.invoke(motionEvent, InputDevice.SOURCE_TOUCHSCREEN); + + motionEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN); } - int eventMode = sync ? syncEventMode : asyncEventMode; - return (Boolean) - injectInputEventMethod.invoke(instanceInputManagerObject, motionEvent, eventMode); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (IllegalArgumentException e) { - throw e; - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); + int eventMode = + sync ? INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH : INJECT_INPUT_EVENT_MODE_ASYNC; + return injectInputEventMethod.invoke(getInputManager(), motionEvent, eventMode); + } catch (ReflectionException e) { + Throwable cause = e.getCause().getCause(); if (cause instanceof SecurityException) { if (shouldRetry) { Log.w( @@ -164,7 +124,7 @@ private boolean innerInjectMotionEvent(MotionEvent motionEvent, boolean shouldRe cause); } } else { - throw new RuntimeException(e); + throw new RuntimeException(e.getCause()); } } catch (SecurityException e) { throw new InjectEventSecurityException(e); @@ -179,4 +139,32 @@ private static boolean isFromTouchpadInGlassDevice(MotionEvent motionEvent) { || Build.DEVICE.contains("wingman")) && ((motionEvent.getSource() & InputDevice.SOURCE_TOUCHPAD) != 0); } + + private InputManager getInputManager() { + if (Build.VERSION.SDK_INT < 23) { + return getInstanceMethod.invokeStatic(); + } else { + return getContext().getSystemService(InputManager.class); + } + } + + private static Context getContext() { + try { + return InstrumentationRegistry.getInstrumentation().getTargetContext(); + } catch (IllegalStateException e) { + // Espresso is being used outside of instrumentation. Unusual, but prior art exists + // Attempt to get context from global views + try { + List views = WindowInspectorCompat.getGlobalWindowViews(); + if (views.isEmpty()) { + throw new IllegalStateException( + "Could not get Context. Not running under instrumentation and there is no UI" + + " present"); + } + return views.get(0).getContext(); + } catch (ViewRetrievalException ve) { + throw new IllegalStateException(ve); + } + } + } } diff --git a/espresso/core/javatests/androidx/test/espresso/base/EventInjectorTest.java b/espresso/core/javatests/androidx/test/espresso/base/EventInjectorTest.java index e8541d9b8..86793a386 100644 --- a/espresso/core/javatests/androidx/test/espresso/base/EventInjectorTest.java +++ b/espresso/core/javatests/androidx/test/espresso/base/EventInjectorTest.java @@ -45,7 +45,7 @@ public class EventInjectorTest { @Before public void setUp() throws Exception { - injector = new EventInjector(new InputManagerEventInjectionStrategy().initialize()); + injector = new EventInjector(new InputManagerEventInjectionStrategy()); } @Test diff --git a/espresso/core/javatests/androidx/test/espresso/base/UiControllerImplIntegrationTest.java b/espresso/core/javatests/androidx/test/espresso/base/UiControllerImplIntegrationTest.java index 3ac47b460..0f0bd5ec4 100644 --- a/espresso/core/javatests/androidx/test/espresso/base/UiControllerImplIntegrationTest.java +++ b/espresso/core/javatests/androidx/test/espresso/base/UiControllerImplIntegrationTest.java @@ -57,8 +57,7 @@ public class UiControllerImplIntegrationTest { @Before public void setUp() throws Exception { - EventInjector injector = - new EventInjector(new InputManagerEventInjectionStrategy().initialize()); + EventInjector injector = new EventInjector(new InputManagerEventInjectionStrategy()); uiController = new UiControllerImpl( diff --git a/espresso/core/javatests/androidx/test/espresso/base/UiControllerImplTest.java b/espresso/core/javatests/androidx/test/espresso/base/UiControllerImplTest.java index f0b4137bd..2a2062cab 100644 --- a/espresso/core/javatests/androidx/test/espresso/base/UiControllerImplTest.java +++ b/espresso/core/javatests/androidx/test/espresso/base/UiControllerImplTest.java @@ -108,8 +108,7 @@ public void uncaughtException(Thread thread, Throwable ex) { new IdlingResourceRegistry(testThread.getLooper(), Tracing.getInstance()); asyncPool = new ThreadPoolExecutor(3, 3, 1, TimeUnit.SECONDS, new LinkedBlockingQueue()); - EventInjector injector = - new EventInjector(new InputManagerEventInjectionStrategy().initialize()); + EventInjector injector = new EventInjector(new InputManagerEventInjectionStrategy()); uiController.set( new UiControllerImpl(