blob: b6d09d80271b8e7e929f59e97852d9e2e767e688 [file] [log] [blame]
/*
* Copyright (C) 2016 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 android.cts.util;
import android.app.Instrumentation;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import java.lang.reflect.Field;
/**
* Utility class to send KeyEvents to TextView bypassing the IME. The code is similar to functions
* in {@link Instrumentation} and {@link android.test.InstrumentationTestCase} classes. It uses
* {@link InputMethodManager#dispatchKeyEventFromInputMethod(View, KeyEvent)} to send the events.
* After sending the events waits for idle.
*/
public class KeyEventUtil {
private final Instrumentation mInstrumentation;
public KeyEventUtil(Instrumentation instrumentation) {
this.mInstrumentation = instrumentation;
}
/**
* Sends the key events corresponding to the text to the app being instrumented.
*
* @param targetView View to find the ViewRootImpl and dispatch.
* @param text The text to be sent. Null value returns immediately.
*/
public final void sendString(final View targetView, final String text) {
if (text == null) {
return;
}
KeyEvent[] events = getKeyEvents(text);
if (events != null) {
for (int i = 0; i < events.length; i++) {
// We have to change the time of an event before injecting it because
// all KeyEvents returned by KeyCharacterMap.getEvents() have the same
// time stamp and the system rejects too old events. Hence, it is
// possible for an event to become stale before it is injected if it
// takes too long to inject the preceding ones.
sendKey(targetView, KeyEvent.changeTimeRepeat(events[i], SystemClock.uptimeMillis(),
0));
}
}
}
/**
* Sends a series of key events through instrumentation. For instance:
* sendKeys(view, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_CENTER).
*
* @param targetView View to find the ViewRootImpl and dispatch.
* @param keys The series of key codes.
*/
public final void sendKeys(final View targetView, final int...keys) {
final int count = keys.length;
for (int i = 0; i < count; i++) {
try {
sendKeyDownUp(targetView, keys[i]);
} catch (SecurityException e) {
// Ignore security exceptions that are now thrown
// when trying to send to another app, to retain
// compatibility with existing tests.
}
}
}
/**
* Sends a series of key events through instrumentation. The sequence of keys is a string
* containing the key names as specified in KeyEvent, without the KEYCODE_ prefix. For
* instance: sendKeys(view, "DPAD_LEFT A B C DPAD_CENTER"). Each key can be repeated by using
* the N* prefix. For instance, to send two KEYCODE_DPAD_LEFT, use the following:
* sendKeys(view, "2*DPAD_LEFT").
*
* @param targetView View to find the ViewRootImpl and dispatch.
* @param keysSequence The sequence of keys.
*/
public final void sendKeys(final View targetView, final String keysSequence) {
final String[] keys = keysSequence.split(" ");
final int count = keys.length;
for (int i = 0; i < count; i++) {
String key = keys[i];
int repeater = key.indexOf('*');
int keyCount;
try {
keyCount = repeater == -1 ? 1 : Integer.parseInt(key.substring(0, repeater));
} catch (NumberFormatException e) {
Log.w("ActivityTestCase", "Invalid repeat count: " + key);
continue;
}
if (repeater != -1) {
key = key.substring(repeater + 1);
}
for (int j = 0; j < keyCount; j++) {
try {
final Field keyCodeField = KeyEvent.class.getField("KEYCODE_" + key);
final int keyCode = keyCodeField.getInt(null);
try {
sendKeyDownUp(targetView, keyCode);
} catch (SecurityException e) {
// Ignore security exceptions that are now thrown
// when trying to send to another app, to retain
// compatibility with existing tests.
}
} catch (NoSuchFieldException e) {
Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
break;
} catch (IllegalAccessException e) {
Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
break;
}
}
}
}
/**
* Sends an up and down key events.
*
* @param targetView View to find the ViewRootImpl and dispatch.
* @param key The integer keycode for the event to be send.
*/
public final void sendKeyDownUp(final View targetView, final int key) {
sendKey(targetView, new KeyEvent(KeyEvent.ACTION_DOWN, key));
sendKey(targetView, new KeyEvent(KeyEvent.ACTION_UP, key));
}
/**
* Sends a key event.
*
* @param targetView View to find the ViewRootImpl and dispatch.
* @param event KeyEvent to be send.
*/
public final void sendKey(final View targetView, final KeyEvent event) {
validateNotAppThread();
long downTime = event.getDownTime();
long eventTime = event.getEventTime();
int action = event.getAction();
int code = event.getKeyCode();
int repeatCount = event.getRepeatCount();
int metaState = event.getMetaState();
int deviceId = event.getDeviceId();
int scancode = event.getScanCode();
int source = event.getSource();
int flags = event.getFlags();
if (source == InputDevice.SOURCE_UNKNOWN) {
source = InputDevice.SOURCE_KEYBOARD;
}
if (eventTime == 0) {
eventTime = SystemClock.uptimeMillis();
}
if (downTime == 0) {
downTime = eventTime;
}
final KeyEvent newEvent = new KeyEvent(downTime, eventTime, action, code, repeatCount,
metaState, deviceId, scancode, flags, source);
InputMethodManager imm = targetView.getContext().getSystemService(InputMethodManager.class);
imm.dispatchKeyEventFromInputMethod(null, newEvent);
mInstrumentation.waitForIdleSync();
}
private KeyEvent[] getKeyEvents(final String text) {
KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
return keyCharacterMap.getEvents(text.toCharArray());
}
private void validateNotAppThread() {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException(
"This method can not be called from the main application thread");
}
}
}