1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.compatibility.common.util;
18 
19 import android.app.Instrumentation;
20 import android.os.Looper;
21 import android.os.SystemClock;
22 import android.util.Log;
23 import android.view.InputDevice;
24 import android.view.KeyCharacterMap;
25 import android.view.KeyEvent;
26 import android.view.View;
27 import android.view.inputmethod.InputMethodManager;
28 
29 import java.lang.reflect.Field;
30 
31 /**
32  * Utility class to send KeyEvents bypassing the IME. The code is similar to functions in
33  * {@link Instrumentation} and {@link android.test.InstrumentationTestCase} classes. It uses
34  * {@link InputMethodManager#dispatchKeyEventFromInputMethod(View, KeyEvent)} to send the events.
35  * After sending the events waits for idle.
36  */
37 public final class CtsKeyEventUtil {
38 
CtsKeyEventUtil()39     private CtsKeyEventUtil() {}
40 
41     /**
42      * Sends the key events corresponding to the text to the app being instrumented.
43      *
44      * @param instrumentation the instrumentation used to run the test.
45      * @param targetView View to find the ViewRootImpl and dispatch.
46      * @param text The text to be sent. Null value returns immediately.
47      */
sendString(final Instrumentation instrumentation, final View targetView, final String text)48     public static void sendString(final Instrumentation instrumentation, final View targetView,
49             final String text) {
50         if (text == null) {
51             return;
52         }
53 
54         KeyEvent[] events = getKeyEvents(text);
55 
56         if (events != null) {
57             for (int i = 0; i < events.length; i++) {
58                 // We have to change the time of an event before injecting it because
59                 // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
60                 // time stamp and the system rejects too old events. Hence, it is
61                 // possible for an event to become stale before it is injected if it
62                 // takes too long to inject the preceding ones.
63                 sendKey(instrumentation, targetView, KeyEvent.changeTimeRepeat(
64                         events[i], SystemClock.uptimeMillis(), 0 /* newRepeat */));
65             }
66         }
67     }
68 
69     /**
70      * Sends a series of key events through instrumentation. For instance:
71      * sendKeys(view, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_CENTER).
72      *
73      * @param instrumentation the instrumentation used to run the test.
74      * @param targetView View to find the ViewRootImpl and dispatch.
75      * @param keys The series of key codes.
76      */
sendKeys(final Instrumentation instrumentation, final View targetView, final int...keys)77     public static void sendKeys(final Instrumentation instrumentation, final View targetView,
78             final int...keys) {
79         final int count = keys.length;
80 
81         for (int i = 0; i < count; i++) {
82             try {
83                 sendKeyDownUp(instrumentation, targetView, keys[i]);
84             } catch (SecurityException e) {
85                 // Ignore security exceptions that are now thrown
86                 // when trying to send to another app, to retain
87                 // compatibility with existing tests.
88             }
89         }
90     }
91 
92     /**
93      * Sends a series of key events through instrumentation. The sequence of keys is a string
94      * containing the key names as specified in KeyEvent, without the KEYCODE_ prefix. For
95      * instance: sendKeys(view, "DPAD_LEFT A B C DPAD_CENTER"). Each key can be repeated by using
96      * the N* prefix. For instance, to send two KEYCODE_DPAD_LEFT, use the following:
97      * sendKeys(view, "2*DPAD_LEFT").
98      *
99      * @param instrumentation the instrumentation used to run the test.
100      * @param targetView View to find the ViewRootImpl and dispatch.
101      * @param keysSequence The sequence of keys.
102      */
sendKeys(final Instrumentation instrumentation, final View targetView, final String keysSequence)103     public static void sendKeys(final Instrumentation instrumentation, final View targetView,
104             final String keysSequence) {
105         final String[] keys = keysSequence.split(" ");
106         final int count = keys.length;
107 
108         for (int i = 0; i < count; i++) {
109             String key = keys[i];
110             int repeater = key.indexOf('*');
111 
112             int keyCount;
113             try {
114                 keyCount = repeater == -1 ? 1 : Integer.parseInt(key.substring(0, repeater));
115             } catch (NumberFormatException e) {
116                 Log.w("ActivityTestCase", "Invalid repeat count: " + key);
117                 continue;
118             }
119 
120             if (repeater != -1) {
121                 key = key.substring(repeater + 1);
122             }
123 
124             for (int j = 0; j < keyCount; j++) {
125                 try {
126                     final Field keyCodeField = KeyEvent.class.getField("KEYCODE_" + key);
127                     final int keyCode = keyCodeField.getInt(null);
128                     try {
129                         sendKeyDownUp(instrumentation, targetView, keyCode);
130                     } catch (SecurityException e) {
131                         // Ignore security exceptions that are now thrown
132                         // when trying to send to another app, to retain
133                         // compatibility with existing tests.
134                     }
135                 } catch (NoSuchFieldException e) {
136                     Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
137                     break;
138                 } catch (IllegalAccessException e) {
139                     Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
140                     break;
141                 }
142             }
143         }
144     }
145 
146     /**
147      * Sends an up and down key events.
148      *
149      * @param instrumentation the instrumentation used to run the test.
150      * @param targetView View to find the ViewRootImpl and dispatch.
151      * @param key The integer keycode for the event to be sent.
152      */
sendKeyDownUp(final Instrumentation instrumentation, final View targetView, final int key)153     public static void sendKeyDownUp(final Instrumentation instrumentation, final View targetView,
154             final int key) {
155         sendKey(instrumentation, targetView, new KeyEvent(KeyEvent.ACTION_DOWN, key),
156                 false /* waitForIdle */);
157         sendKey(instrumentation, targetView, new KeyEvent(KeyEvent.ACTION_UP, key));
158     }
159 
160     /**
161      * Sends a key event.
162      *
163      * @param instrumentation the instrumentation used to run the test.
164      * @param targetView View to find the ViewRootImpl and dispatch.
165      * @param event KeyEvent to be send.
166      */
sendKey(final Instrumentation instrumentation, final View targetView, final KeyEvent event)167     public static void sendKey(final Instrumentation instrumentation, final View targetView,
168             final KeyEvent event) {
169         sendKey(instrumentation, targetView, event, true /* waitForIdle */);
170     }
171 
sendKey(final Instrumentation instrumentation, final View targetView, final KeyEvent event, boolean waitForIdle)172     private static void sendKey(final Instrumentation instrumentation, final View targetView,
173             final KeyEvent event, boolean waitForIdle) {
174         validateNotAppThread();
175 
176         long downTime = event.getDownTime();
177         long eventTime = event.getEventTime();
178         int action = event.getAction();
179         int code = event.getKeyCode();
180         int repeatCount = event.getRepeatCount();
181         int metaState = event.getMetaState();
182         int deviceId = event.getDeviceId();
183         int scanCode = event.getScanCode();
184         int source = event.getSource();
185         int flags = event.getFlags();
186         if (source == InputDevice.SOURCE_UNKNOWN) {
187             source = InputDevice.SOURCE_KEYBOARD;
188         }
189         if (eventTime == 0) {
190             eventTime = SystemClock.uptimeMillis();
191         }
192         if (downTime == 0) {
193             downTime = eventTime;
194         }
195 
196         final KeyEvent newEvent = new KeyEvent(downTime, eventTime, action, code, repeatCount,
197                 metaState, deviceId, scanCode, flags, source);
198 
199         InputMethodManager imm = targetView.getContext().getSystemService(InputMethodManager.class);
200         imm.dispatchKeyEventFromInputMethod(imm.isActive() ? null : targetView, newEvent);
201         if (waitForIdle) {
202             instrumentation.waitForIdleSync();
203         }
204     }
205 
206     /**
207      * Sends a key event while holding another modifier key down, then releases both keys and
208      * waits for idle sync. Useful for sending combinations like shift + tab.
209      *
210      * @param instrumentation the instrumentation used to run the test.
211      * @param targetView View to find the ViewRootImpl and dispatch.
212      * @param keyCodeToSend The integer keycode for the event to be sent.
213      * @param modifierKeyCodeToHold The integer keycode of the modifier to be held.
214      */
sendKeyWhileHoldingModifier(final Instrumentation instrumentation, final View targetView, final int keyCodeToSend, final int modifierKeyCodeToHold)215     public static void sendKeyWhileHoldingModifier(final Instrumentation instrumentation,
216             final View targetView, final int keyCodeToSend,
217             final int modifierKeyCodeToHold) {
218         final int metaState = getMetaStateForModifierKeyCode(modifierKeyCodeToHold);
219         final long downTime = SystemClock.uptimeMillis();
220 
221         final KeyEvent holdKeyDown = new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
222                 modifierKeyCodeToHold, 0 /* repeat */);
223         sendKey(instrumentation ,targetView, holdKeyDown);
224 
225         final KeyEvent keyDown = new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
226                 keyCodeToSend, 0 /* repeat */, metaState);
227         sendKey(instrumentation, targetView, keyDown);
228 
229         final KeyEvent keyUp = new KeyEvent(downTime, downTime, KeyEvent.ACTION_UP,
230                 keyCodeToSend, 0 /* repeat */, metaState);
231         sendKey(instrumentation, targetView, keyUp);
232 
233         final KeyEvent holdKeyUp = new KeyEvent(downTime, downTime, KeyEvent.ACTION_UP,
234                 modifierKeyCodeToHold, 0 /* repeat */);
235         sendKey(instrumentation, targetView, holdKeyUp);
236 
237         instrumentation.waitForIdleSync();
238     }
239 
getMetaStateForModifierKeyCode(int modifierKeyCode)240     private static int getMetaStateForModifierKeyCode(int modifierKeyCode) {
241         if (!KeyEvent.isModifierKey(modifierKeyCode)) {
242             throw new IllegalArgumentException("Modifier key expected, but got: "
243                     + KeyEvent.keyCodeToString(modifierKeyCode));
244         }
245 
246         int metaState;
247         switch (modifierKeyCode) {
248             case KeyEvent.KEYCODE_SHIFT_LEFT:
249                 metaState = KeyEvent.META_SHIFT_LEFT_ON;
250                 break;
251             case KeyEvent.KEYCODE_SHIFT_RIGHT:
252                 metaState = KeyEvent.META_SHIFT_RIGHT_ON;
253                 break;
254             case KeyEvent.KEYCODE_ALT_LEFT:
255                 metaState = KeyEvent.META_ALT_LEFT_ON;
256                 break;
257             case KeyEvent.KEYCODE_ALT_RIGHT:
258                 metaState = KeyEvent.META_ALT_RIGHT_ON;
259                 break;
260             case KeyEvent.KEYCODE_CTRL_LEFT:
261                 metaState = KeyEvent.META_CTRL_LEFT_ON;
262                 break;
263             case KeyEvent.KEYCODE_CTRL_RIGHT:
264                 metaState = KeyEvent.META_CTRL_RIGHT_ON;
265                 break;
266             case KeyEvent.KEYCODE_META_LEFT:
267                 metaState = KeyEvent.META_META_LEFT_ON;
268                 break;
269             case KeyEvent.KEYCODE_META_RIGHT:
270                 metaState = KeyEvent.META_META_RIGHT_ON;
271                 break;
272             case KeyEvent.KEYCODE_SYM:
273                 metaState = KeyEvent.META_SYM_ON;
274                 break;
275             case KeyEvent.KEYCODE_NUM:
276                 metaState = KeyEvent.META_NUM_LOCK_ON;
277                 break;
278             case KeyEvent.KEYCODE_FUNCTION:
279                 metaState = KeyEvent.META_FUNCTION_ON;
280                 break;
281             default:
282                 // Safety net: all modifier keys need to have at least one meta state associated.
283                 throw new UnsupportedOperationException("No meta state associated with "
284                         + "modifier key: " + KeyEvent.keyCodeToString(modifierKeyCode));
285         }
286 
287         return KeyEvent.normalizeMetaState(metaState);
288     }
289 
getKeyEvents(final String text)290     private static KeyEvent[] getKeyEvents(final String text) {
291         KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
292         return keyCharacterMap.getEvents(text.toCharArray());
293     }
294 
validateNotAppThread()295     private static void validateNotAppThread() {
296         if (Looper.myLooper() == Looper.getMainLooper()) {
297             throw new RuntimeException(
298                     "This method can not be called from the main application thread");
299         }
300     }
301 }
302