blob: 97e43103a935a9c705a3fc58304f9168d6306a06 [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 com.android.compatibility.common.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 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 final class CtsKeyEventUtil {
private CtsKeyEventUtil() {}
/**
* Sends the key events corresponding to the text to the app being instrumented.
*
* @param instrumentation the instrumentation used to run the test.
* @param targetView View to find the ViewRootImpl and dispatch.
* @param text The text to be sent. Null value returns immediately.
*/
public static void sendString(final Instrumentation instrumentation, 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(instrumentation, targetView, KeyEvent.changeTimeRepeat(
events[i], SystemClock.uptimeMillis(), 0 /* newRepeat */));
}
}
}
/**
* Sends a series of key events through instrumentation. For instance:
* sendKeys(view, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_CENTER).
*
* @param instrumentation the instrumentation used to run the test.
* @param targetView View to find the ViewRootImpl and dispatch.
* @param keys The series of key codes.
*/
public static void sendKeys(final Instrumentation instrumentation, final View targetView,
final int...keys) {
final int count = keys.length;
for (int i = 0; i < count; i++) {
try {
sendKeyDownUp(instrumentation, 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 instrumentation the instrumentation used to run the test.
* @param targetView View to find the ViewRootImpl and dispatch.
* @param keysSequence The sequence of keys.
*/
public static void sendKeys(final Instrumentation instrumentation, 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(instrumentation, 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 instrumentation the instrumentation used to run the test.
* @param targetView View to find the ViewRootImpl and dispatch.
* @param key The integer keycode for the event to be sent.
*/
public static void sendKeyDownUp(final Instrumentation instrumentation, final View targetView,
final int key) {
sendKey(instrumentation, targetView, new KeyEvent(KeyEvent.ACTION_DOWN, key));
sendKey(instrumentation, targetView, new KeyEvent(KeyEvent.ACTION_UP, key));
}
/**
* Sends a key event.
*
* @param instrumentation the instrumentation used to run the test.
* @param targetView View to find the ViewRootImpl and dispatch.
* @param event KeyEvent to be send.
*/
public static void sendKey(final Instrumentation instrumentation, 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);
instrumentation.waitForIdleSync();
}
/**
* Sends a key event while holding another modifier key down, then releases both keys and
* waits for idle sync. Useful for sending combinations like shift + tab.
*
* @param instrumentation the instrumentation used to run the test.
* @param targetView View to find the ViewRootImpl and dispatch.
* @param keyCodeToSend The integer keycode for the event to be sent.
* @param modifierKeyCodeToHold The integer keycode of the modifier to be held.
*/
public static void sendKeyWhileHoldingModifier(final Instrumentation instrumentation,
final View targetView, final int keyCodeToSend,
final int modifierKeyCodeToHold) {
final int metaState = getMetaStateForModifierKeyCode(modifierKeyCodeToHold);
final long downTime = SystemClock.uptimeMillis();
final KeyEvent holdKeyDown = new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
modifierKeyCodeToHold, 0 /* repeat */);
sendKey(instrumentation ,targetView, holdKeyDown);
final KeyEvent keyDown = new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
keyCodeToSend, 0 /* repeat */, metaState);
sendKey(instrumentation, targetView, keyDown);
final KeyEvent keyUp = new KeyEvent(downTime, downTime, KeyEvent.ACTION_UP,
keyCodeToSend, 0 /* repeat */, metaState);
sendKey(instrumentation, targetView, keyUp);
final KeyEvent holdKeyUp = new KeyEvent(downTime, downTime, KeyEvent.ACTION_UP,
modifierKeyCodeToHold, 0 /* repeat */);
sendKey(instrumentation, targetView, holdKeyUp);
instrumentation.waitForIdleSync();
}
private static int getMetaStateForModifierKeyCode(int modifierKeyCode) {
if (!KeyEvent.isModifierKey(modifierKeyCode)) {
throw new IllegalArgumentException("Modifier key expected, but got: "
+ KeyEvent.keyCodeToString(modifierKeyCode));
}
int metaState;
switch (modifierKeyCode) {
case KeyEvent.KEYCODE_SHIFT_LEFT:
metaState = KeyEvent.META_SHIFT_LEFT_ON;
break;
case KeyEvent.KEYCODE_SHIFT_RIGHT:
metaState = KeyEvent.META_SHIFT_RIGHT_ON;
break;
case KeyEvent.KEYCODE_ALT_LEFT:
metaState = KeyEvent.META_ALT_LEFT_ON;
break;
case KeyEvent.KEYCODE_ALT_RIGHT:
metaState = KeyEvent.META_ALT_RIGHT_ON;
break;
case KeyEvent.KEYCODE_CTRL_LEFT:
metaState = KeyEvent.META_CTRL_LEFT_ON;
break;
case KeyEvent.KEYCODE_CTRL_RIGHT:
metaState = KeyEvent.META_CTRL_RIGHT_ON;
break;
case KeyEvent.KEYCODE_META_LEFT:
metaState = KeyEvent.META_META_LEFT_ON;
break;
case KeyEvent.KEYCODE_META_RIGHT:
metaState = KeyEvent.META_META_RIGHT_ON;
break;
case KeyEvent.KEYCODE_SYM:
metaState = KeyEvent.META_SYM_ON;
break;
case KeyEvent.KEYCODE_NUM:
metaState = KeyEvent.META_NUM_LOCK_ON;
break;
case KeyEvent.KEYCODE_FUNCTION:
metaState = KeyEvent.META_FUNCTION_ON;
break;
default:
// Safety net: all modifier keys need to have at least one meta state associated.
throw new UnsupportedOperationException("No meta state associated with "
+ "modifier key: " + KeyEvent.keyCodeToString(modifierKeyCode));
}
return KeyEvent.normalizeMetaState(metaState);
}
private static KeyEvent[] getKeyEvents(final String text) {
KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
return keyCharacterMap.getEvents(text.toCharArray());
}
private static void validateNotAppThread() {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException(
"This method can not be called from the main application thread");
}
}
}