/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.espresso.base;
import static com.google.common.base.Preconditions.checkState;
import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import androidx.test.espresso.InjectEventSecurityException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* An {@link EventInjectionStrategy} that uses the input manager to inject Events. This strategy
* supports API level 16 and above.
*/
final class InputManagerEventInjectionStrategy implements EventInjectionStrategy {
private static final String TAG = InputManagerEventInjectionStrategy.class.getSimpleName();
// The delay time to allow the soft keyboard to dismiss.
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;
InputManagerEventInjectionStrategy() {
checkState(Build.VERSION.SDK_INT >= 16, "Unsupported API level.");
}
void initialize() {
if (initComplete) {
return;
}
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);
}
}
@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();
if (cause instanceof SecurityException) {
throw new InjectEventSecurityException(cause);
}
throw new RuntimeException(e);
} catch (SecurityException e) {
throw new InjectEventSecurityException(e);
}
}
@Override
public boolean injectMotionEvent(MotionEvent motionEvent, boolean sync)
throws InjectEventSecurityException {
return innerInjectMotionEvent(motionEvent, true, sync);
}
private boolean innerInjectMotionEvent(MotionEvent motionEvent, boolean shouldRetry, boolean sync)
throws InjectEventSecurityException {
try {
// Need to set the event source to touch screen, otherwise the input can be ignored even
// though injecting it would be successful.
// 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);
}
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();
if (cause instanceof SecurityException) {
if (shouldRetry) {
Log.w(
TAG,
"Error performing a ViewAction! soft keyboard dismissal animation may have "
+ "been in the way. Retrying once after: "
+ KEYBOARD_DISMISSAL_DELAY_MILLIS
+ " millis");
SystemClock.sleep(KEYBOARD_DISMISSAL_DELAY_MILLIS);
innerInjectMotionEvent(motionEvent, false, sync);
} else {
throw new InjectEventSecurityException(cause);
}
} else {
throw new RuntimeException(e);
}
} catch (SecurityException e) {
throw new InjectEventSecurityException(e);
}
return false;
}
// We'd like to inject non-pointer events sourced from touchpad in Glass.
private static boolean isFromTouchpadInGlassDevice(MotionEvent motionEvent) {
return (Build.DEVICE.contains("glass")
|| Build.DEVICE.contains("Glass")
|| Build.DEVICE.contains("wingman"))
&& ((motionEvent.getSource() & InputDevice.SOURCE_TOUCHPAD) != 0);
}
}