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 android.cts.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 to TextView bypassing the IME. The code is similar to functions
33  * in {@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 class KeyEventUtil {
38     private final Instrumentation mInstrumentation;
39 
KeyEventUtil(Instrumentation instrumentation)40     public KeyEventUtil(Instrumentation instrumentation) {
41         this.mInstrumentation = instrumentation;
42     }
43 
44     /**
45      * Sends the key events corresponding to the text to the app being instrumented.
46      *
47      * @param targetView View to find the ViewRootImpl and dispatch.
48      * @param text The text to be sent. Null value returns immediately.
49      */
sendString(final View targetView, final String text)50     public final void sendString(final View targetView, final String text) {
51         if (text == null) {
52             return;
53         }
54 
55         KeyEvent[] events = getKeyEvents(text);
56 
57         if (events != null) {
58             for (int i = 0; i < events.length; i++) {
59                 // We have to change the time of an event before injecting it because
60                 // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
61                 // time stamp and the system rejects too old events. Hence, it is
62                 // possible for an event to become stale before it is injected if it
63                 // takes too long to inject the preceding ones.
64                 sendKey(targetView, KeyEvent.changeTimeRepeat(events[i], SystemClock.uptimeMillis(),
65                         0));
66             }
67         }
68     }
69 
70     /**
71      * Sends a series of key events through instrumentation. For instance:
72      * sendKeys(view, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_CENTER).
73      *
74      * @param targetView View to find the ViewRootImpl and dispatch.
75      * @param keys The series of key codes.
76      */
sendKeys(final View targetView, final int...keys)77     public final void sendKeys(final View targetView, final int...keys) {
78         final int count = keys.length;
79 
80         for (int i = 0; i < count; i++) {
81             try {
82                 sendKeyDownUp(targetView, keys[i]);
83             } catch (SecurityException e) {
84                 // Ignore security exceptions that are now thrown
85                 // when trying to send to another app, to retain
86                 // compatibility with existing tests.
87             }
88         }
89     }
90 
91     /**
92      * Sends a series of key events through instrumentation. The sequence of keys is a string
93      * containing the key names as specified in KeyEvent, without the KEYCODE_ prefix. For
94      * instance: sendKeys(view, "DPAD_LEFT A B C DPAD_CENTER"). Each key can be repeated by using
95      * the N* prefix. For instance, to send two KEYCODE_DPAD_LEFT, use the following:
96      * sendKeys(view, "2*DPAD_LEFT").
97      *
98      * @param targetView View to find the ViewRootImpl and dispatch.
99      * @param keysSequence The sequence of keys.
100      */
sendKeys(final View targetView, final String keysSequence)101     public final void sendKeys(final View targetView, final String keysSequence) {
102         final String[] keys = keysSequence.split(" ");
103         final int count = keys.length;
104 
105         for (int i = 0; i < count; i++) {
106             String key = keys[i];
107             int repeater = key.indexOf('*');
108 
109             int keyCount;
110             try {
111                 keyCount = repeater == -1 ? 1 : Integer.parseInt(key.substring(0, repeater));
112             } catch (NumberFormatException e) {
113                 Log.w("ActivityTestCase", "Invalid repeat count: " + key);
114                 continue;
115             }
116 
117             if (repeater != -1) {
118                 key = key.substring(repeater + 1);
119             }
120 
121             for (int j = 0; j < keyCount; j++) {
122                 try {
123                     final Field keyCodeField = KeyEvent.class.getField("KEYCODE_" + key);
124                     final int keyCode = keyCodeField.getInt(null);
125                     try {
126                         sendKeyDownUp(targetView, keyCode);
127                     } catch (SecurityException e) {
128                         // Ignore security exceptions that are now thrown
129                         // when trying to send to another app, to retain
130                         // compatibility with existing tests.
131                     }
132                 } catch (NoSuchFieldException e) {
133                     Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
134                     break;
135                 } catch (IllegalAccessException e) {
136                     Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
137                     break;
138                 }
139             }
140         }
141     }
142 
143     /**
144      * Sends an up and down key events.
145      *
146      * @param targetView View to find the ViewRootImpl and dispatch.
147      * @param key The integer keycode for the event to be send.
148      */
sendKeyDownUp(final View targetView, final int key)149     public final void sendKeyDownUp(final View targetView, final int key) {
150         sendKey(targetView, new KeyEvent(KeyEvent.ACTION_DOWN, key));
151         sendKey(targetView, new KeyEvent(KeyEvent.ACTION_UP, key));
152     }
153 
154     /**
155      * Sends a key event.
156      *
157      * @param targetView View to find the ViewRootImpl and dispatch.
158      * @param event KeyEvent to be send.
159      */
sendKey(final View targetView, final KeyEvent event)160     public final void sendKey(final View targetView, final KeyEvent event) {
161         validateNotAppThread();
162 
163         long downTime = event.getDownTime();
164         long eventTime = event.getEventTime();
165         int action = event.getAction();
166         int code = event.getKeyCode();
167         int repeatCount = event.getRepeatCount();
168         int metaState = event.getMetaState();
169         int deviceId = event.getDeviceId();
170         int scancode = event.getScanCode();
171         int source = event.getSource();
172         int flags = event.getFlags();
173         if (source == InputDevice.SOURCE_UNKNOWN) {
174             source = InputDevice.SOURCE_KEYBOARD;
175         }
176         if (eventTime == 0) {
177             eventTime = SystemClock.uptimeMillis();
178         }
179         if (downTime == 0) {
180             downTime = eventTime;
181         }
182 
183         final KeyEvent newEvent = new KeyEvent(downTime, eventTime, action, code, repeatCount,
184                 metaState, deviceId, scancode, flags, source);
185 
186         InputMethodManager imm = targetView.getContext().getSystemService(InputMethodManager.class);
187         imm.dispatchKeyEventFromInputMethod(null, newEvent);
188         mInstrumentation.waitForIdleSync();
189     }
190 
getKeyEvents(final String text)191     private KeyEvent[] getKeyEvents(final String text) {
192         KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
193         return keyCharacterMap.getEvents(text.toCharArray());
194     }
195 
validateNotAppThread()196     private void validateNotAppThread() {
197         if (Looper.myLooper() == Looper.getMainLooper()) {
198             throw new RuntimeException(
199                     "This method can not be called from the main application thread");
200         }
201     }
202 }
203