• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 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.server.input;
18 
19 import static android.view.Display.DEFAULT_DISPLAY;
20 import static android.view.Display.INVALID_DISPLAY;
21 import static android.view.KeyEvent.KEYCODE_ALT_LEFT;
22 import static android.view.KeyEvent.KEYCODE_ALT_RIGHT;
23 import static android.view.KeyEvent.KEYCODE_CTRL_LEFT;
24 import static android.view.KeyEvent.KEYCODE_CTRL_RIGHT;
25 import static android.view.KeyEvent.KEYCODE_META_LEFT;
26 import static android.view.KeyEvent.KEYCODE_META_RIGHT;
27 import static android.view.KeyEvent.KEYCODE_SHIFT_LEFT;
28 import static android.view.KeyEvent.KEYCODE_SHIFT_RIGHT;
29 import static android.view.KeyEvent.META_ALT_LEFT_ON;
30 import static android.view.KeyEvent.META_ALT_ON;
31 import static android.view.KeyEvent.META_ALT_RIGHT_ON;
32 import static android.view.KeyEvent.META_CTRL_LEFT_ON;
33 import static android.view.KeyEvent.META_CTRL_ON;
34 import static android.view.KeyEvent.META_CTRL_RIGHT_ON;
35 import static android.view.KeyEvent.META_META_LEFT_ON;
36 import static android.view.KeyEvent.META_META_ON;
37 import static android.view.KeyEvent.META_META_RIGHT_ON;
38 import static android.view.KeyEvent.META_SHIFT_LEFT_ON;
39 import static android.view.KeyEvent.META_SHIFT_ON;
40 import static android.view.KeyEvent.META_SHIFT_RIGHT_ON;
41 
42 import static java.util.Collections.unmodifiableMap;
43 
44 import android.hardware.input.InputManager;
45 import android.os.ShellCommand;
46 import android.os.SystemClock;
47 import android.util.ArrayMap;
48 import android.util.IntArray;
49 import android.view.InputDevice;
50 import android.view.KeyCharacterMap;
51 import android.view.KeyEvent;
52 import android.view.MotionEvent;
53 import android.view.ViewConfiguration;
54 
55 import java.io.PrintWriter;
56 import java.util.Map;
57 
58 /**
59  * Command that sends input events to the device.
60  */
61 
62 public class InputShellCommand extends ShellCommand {
63     private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
64     private static final String INVALID_DISPLAY_ARGUMENTS =
65             "Error: Invalid arguments for display ID.";
66     private static final int DEFAULT_DEVICE_ID = 0;
67     private static final float DEFAULT_PRESSURE = 1.0f;
68     private static final float NO_PRESSURE = 0.0f;
69     private static final float DEFAULT_SIZE = 1.0f;
70     private static final int DEFAULT_META_STATE = 0;
71     private static final float DEFAULT_PRECISION_X = 1.0f;
72     private static final float DEFAULT_PRECISION_Y = 1.0f;
73     private static final int DEFAULT_EDGE_FLAGS = 0;
74     private static final int DEFAULT_BUTTON_STATE = 0;
75     private static final int DEFAULT_FLAGS = 0;
76 
77     /** Modifier key to meta state */
78     private static final Map<Integer, Integer> MODIFIER;
79     static {
80         final Map<Integer, Integer> map = new ArrayMap<>();
map.put(KEYCODE_CTRL_LEFT, META_CTRL_LEFT_ON | META_CTRL_ON)81         map.put(KEYCODE_CTRL_LEFT, META_CTRL_LEFT_ON | META_CTRL_ON);
map.put(KEYCODE_CTRL_RIGHT, META_CTRL_RIGHT_ON | META_CTRL_ON)82         map.put(KEYCODE_CTRL_RIGHT, META_CTRL_RIGHT_ON | META_CTRL_ON);
map.put(KEYCODE_ALT_LEFT, META_ALT_LEFT_ON | META_ALT_ON)83         map.put(KEYCODE_ALT_LEFT, META_ALT_LEFT_ON | META_ALT_ON);
map.put(KEYCODE_ALT_RIGHT, META_ALT_RIGHT_ON | META_ALT_ON)84         map.put(KEYCODE_ALT_RIGHT, META_ALT_RIGHT_ON | META_ALT_ON);
map.put(KEYCODE_SHIFT_LEFT, META_SHIFT_LEFT_ON | META_SHIFT_ON)85         map.put(KEYCODE_SHIFT_LEFT, META_SHIFT_LEFT_ON | META_SHIFT_ON);
map.put(KEYCODE_SHIFT_RIGHT, META_SHIFT_RIGHT_ON | META_SHIFT_ON)86         map.put(KEYCODE_SHIFT_RIGHT, META_SHIFT_RIGHT_ON | META_SHIFT_ON);
map.put(KEYCODE_META_LEFT, META_META_LEFT_ON | META_META_ON)87         map.put(KEYCODE_META_LEFT, META_META_LEFT_ON | META_META_ON);
map.put(KEYCODE_META_RIGHT, META_META_RIGHT_ON | META_META_ON)88         map.put(KEYCODE_META_RIGHT, META_META_RIGHT_ON | META_META_ON);
89 
90         MODIFIER = unmodifiableMap(map);
91     }
92 
93     /** String to device source */
94     private static final Map<String, Integer> SOURCES;
95     static {
96         final Map<String, Integer> map = new ArrayMap<>();
97         map.put("keyboard", InputDevice.SOURCE_KEYBOARD);
98         map.put("dpad", InputDevice.SOURCE_DPAD);
99         map.put("gamepad", InputDevice.SOURCE_GAMEPAD);
100         map.put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
101         map.put("mouse", InputDevice.SOURCE_MOUSE);
102         map.put("stylus", InputDevice.SOURCE_STYLUS);
103         map.put("trackball", InputDevice.SOURCE_TRACKBALL);
104         map.put("touchpad", InputDevice.SOURCE_TOUCHPAD);
105         map.put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
106         map.put("joystick", InputDevice.SOURCE_JOYSTICK);
107 
108         SOURCES = unmodifiableMap(map);
109     }
110 
injectKeyEvent(KeyEvent event)111     private void injectKeyEvent(KeyEvent event) {
112         InputManager.getInstance().injectInputEvent(event,
113                 InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
114     }
115 
getInputDeviceId(int inputSource)116     private int getInputDeviceId(int inputSource) {
117         int[] devIds = InputDevice.getDeviceIds();
118         for (int devId : devIds) {
119             InputDevice inputDev = InputDevice.getDevice(devId);
120             if (inputDev.supportsSource(inputSource)) {
121                 return devId;
122             }
123         }
124         return DEFAULT_DEVICE_ID;
125     }
126 
getDisplayId()127     private int getDisplayId() {
128         String displayArg = getNextArgRequired();
129         if ("INVALID_DISPLAY".equalsIgnoreCase(displayArg)) {
130             return INVALID_DISPLAY;
131         } else if ("DEFAULT_DISPLAY".equalsIgnoreCase(displayArg)) {
132             return DEFAULT_DISPLAY;
133         } else {
134             try {
135                 final int displayId = Integer.parseInt(displayArg);
136                 if (displayId == INVALID_DISPLAY) {
137                     return INVALID_DISPLAY;
138                 }
139                 return Math.max(displayId, 0);
140             } catch (NumberFormatException e) {
141                 throw new IllegalArgumentException(INVALID_DISPLAY_ARGUMENTS);
142             }
143         }
144     }
145 
146     /**
147      * Builds a MotionEvent and injects it into the event stream.
148      *
149      * @param inputSource the InputDevice.SOURCE_* sending the input event
150      * @param action the MotionEvent.ACTION_* for the event
151      * @param downTime the value of the ACTION_DOWN event happened
152      * @param when the value of SystemClock.uptimeMillis() at which the event happened
153      * @param x x coordinate of event
154      * @param y y coordinate of event
155      * @param pressure pressure of event
156      */
injectMotionEvent(int inputSource, int action, long downTime, long when, float x, float y, float pressure, int displayId)157     private void injectMotionEvent(int inputSource, int action, long downTime, long when,
158             float x, float y, float pressure, int displayId) {
159         final int pointerCount = 1;
160         MotionEvent.PointerProperties[] pointerProperties =
161                 new MotionEvent.PointerProperties[pointerCount];
162         MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
163         for (int i = 0; i < pointerCount; i++) {
164             pointerProperties[i] = new MotionEvent.PointerProperties();
165             pointerProperties[i].id = i;
166             pointerProperties[i].toolType = getToolType(inputSource);
167             pointerCoords[i] = new MotionEvent.PointerCoords();
168             pointerCoords[i].x = x;
169             pointerCoords[i].y = y;
170             pointerCoords[i].pressure = pressure;
171             pointerCoords[i].size = DEFAULT_SIZE;
172         }
173         if (displayId == INVALID_DISPLAY
174                 && (inputSource & InputDevice.SOURCE_CLASS_POINTER) != 0) {
175             displayId = DEFAULT_DISPLAY;
176         }
177         MotionEvent event = MotionEvent.obtain(downTime, when, action, pointerCount,
178                 pointerProperties, pointerCoords, DEFAULT_META_STATE, DEFAULT_BUTTON_STATE,
179                 DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, getInputDeviceId(inputSource),
180                 DEFAULT_EDGE_FLAGS, inputSource, displayId, DEFAULT_FLAGS);
181         InputManager.getInstance().injectInputEvent(event,
182                 InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
183     }
184 
lerp(float a, float b, float alpha)185     private float lerp(float a, float b, float alpha) {
186         return (b - a) * alpha + a;
187     }
188 
getSource(int inputSource, int defaultSource)189     private int getSource(int inputSource, int defaultSource) {
190         return inputSource == InputDevice.SOURCE_UNKNOWN ? defaultSource : inputSource;
191     }
192 
getToolType(int inputSource)193     private int getToolType(int inputSource) {
194         switch(inputSource) {
195             case InputDevice.SOURCE_MOUSE:
196             case InputDevice.SOURCE_MOUSE_RELATIVE:
197             case InputDevice.SOURCE_TRACKBALL:
198                 return MotionEvent.TOOL_TYPE_MOUSE;
199 
200             case InputDevice.SOURCE_STYLUS:
201             case InputDevice.SOURCE_BLUETOOTH_STYLUS:
202                 return MotionEvent.TOOL_TYPE_STYLUS;
203 
204             case InputDevice.SOURCE_TOUCHPAD:
205             case InputDevice.SOURCE_TOUCHSCREEN:
206             case InputDevice.SOURCE_TOUCH_NAVIGATION:
207                 return MotionEvent.TOOL_TYPE_FINGER;
208         }
209         return MotionEvent.TOOL_TYPE_UNKNOWN;
210     }
211 
212     @Override
onCommand(String cmd)213     public final int onCommand(String cmd) {
214         String arg = cmd;
215         int inputSource = InputDevice.SOURCE_UNKNOWN;
216         // Get source (optional).
217         if (SOURCES.containsKey(arg)) {
218             inputSource = SOURCES.get(arg);
219             arg = getNextArgRequired();
220         }
221 
222         // Get displayId (optional).
223         int displayId = INVALID_DISPLAY;
224         if ("-d".equals(arg)) {
225             displayId = getDisplayId();
226             arg = getNextArgRequired();
227         }
228 
229         try {
230             if ("text".equals(arg)) {
231                 runText(inputSource, displayId);
232             } else if ("keyevent".equals(arg)) {
233                 runKeyEvent(inputSource, displayId);
234             } else if ("tap".equals(arg)) {
235                 runTap(inputSource, displayId);
236             } else if ("swipe".equals(arg)) {
237                 runSwipe(inputSource, displayId);
238             } else if ("draganddrop".equals(arg)) {
239                 runDragAndDrop(inputSource, displayId);
240             } else if ("press".equals(arg)) {
241                 runPress(inputSource, displayId);
242             } else if ("roll".equals(arg)) {
243                 runRoll(inputSource, displayId);
244             }  else if ("motionevent".equals(arg)) {
245                 runMotionEvent(inputSource, displayId);
246             } else if ("keycombination".equals(arg)) {
247                 runKeyCombination(inputSource, displayId);
248             } else {
249                 handleDefaultCommands(arg);
250             }
251         } catch (NumberFormatException ex) {
252             throw new IllegalArgumentException(INVALID_ARGUMENTS + arg);
253         }
254         return 0;
255     }
256 
257     @Override
onHelp()258     public final void onHelp() {
259         try (PrintWriter out = getOutPrintWriter();) {
260             out.println("Usage: input [<source>] [-d DISPLAY_ID] <command> [<arg>...]");
261             out.println();
262             out.println("The sources are: ");
263             for (String src : SOURCES.keySet()) {
264                 out.println("      " + src);
265             }
266             out.println();
267             out.printf("-d: specify the display ID.\n      (Default: %d for key event, "
268                     + "%d for motion event if not specified.)",
269                     INVALID_DISPLAY, DEFAULT_DISPLAY);
270             out.println();
271             out.println("The commands and default sources are:");
272             out.println("      text <string> (Default: touchscreen)");
273             out.println("      keyevent [--longpress|--doubletap] <key code number or name> ..."
274                     + " (Default: keyboard)");
275             out.println("      tap <x> <y> (Default: touchscreen)");
276             out.println("      swipe <x1> <y1> <x2> <y2> [duration(ms)]"
277                     + " (Default: touchscreen)");
278             out.println("      draganddrop <x1> <y1> <x2> <y2> [duration(ms)]"
279                     + " (Default: touchscreen)");
280             out.println("      press (Default: trackball)");
281             out.println("      roll <dx> <dy> (Default: trackball)");
282             out.println("      motionevent <DOWN|UP|MOVE|CANCEL> <x> <y> (Default: touchscreen)");
283             out.println("      keycombination [-t duration(ms)] <key code 1> <key code 2> ..."
284                     + " (Default: keyboard, the key order is important here.)");
285         }
286     }
287 
runText(int inputSource, int displayId)288     private void runText(int inputSource, int displayId) {
289         inputSource = getSource(inputSource, InputDevice.SOURCE_KEYBOARD);
290         sendText(inputSource, getNextArgRequired(), displayId);
291     }
292 
293     /**
294      * Convert the characters of string text into key event's and send to
295      * device.
296      *
297      * @param text is a string of characters you want to input to the device.
298      */
sendText(int source, final String text, int displayId)299     private void sendText(int source, final String text, int displayId) {
300         final StringBuilder buff = new StringBuilder(text);
301         boolean escapeFlag = false;
302         for (int i = 0; i < buff.length(); i++) {
303             if (escapeFlag) {
304                 escapeFlag = false;
305                 if (buff.charAt(i) == 's') {
306                     buff.setCharAt(i, ' ');
307                     buff.deleteCharAt(--i);
308                 }
309             }
310             if (buff.charAt(i) == '%') {
311                 escapeFlag = true;
312             }
313         }
314 
315         final char[] chars = buff.toString().toCharArray();
316         final KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
317         final KeyEvent[] events = kcm.getEvents(chars);
318         for (int i = 0; i < events.length; i++) {
319             KeyEvent e = events[i];
320             if (source != e.getSource()) {
321                 e.setSource(source);
322             }
323             e.setDisplayId(displayId);
324             injectKeyEvent(e);
325         }
326     }
327 
runKeyEvent(int inputSource, int displayId)328     private void runKeyEvent(int inputSource, int displayId) {
329         String arg = getNextArgRequired();
330         final boolean longpress = "--longpress".equals(arg);
331         if (longpress) {
332             arg = getNextArgRequired();
333         } else {
334             final boolean doubleTap = "--doubletap".equals(arg);
335             if (doubleTap) {
336                 arg = getNextArgRequired();
337                 final int keycode = KeyEvent.keyCodeFromString(arg);
338                 sendKeyDoubleTap(inputSource, keycode, displayId);
339                 return;
340             }
341         }
342 
343         do {
344             final int keycode = KeyEvent.keyCodeFromString(arg);
345             sendKeyEvent(inputSource, keycode, longpress, displayId);
346         } while ((arg = getNextArg()) != null);
347     }
348 
sendKeyEvent(int inputSource, int keyCode, boolean longpress, int displayId)349     private void sendKeyEvent(int inputSource, int keyCode, boolean longpress, int displayId) {
350         final long now = SystemClock.uptimeMillis();
351 
352         KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0 /* repeatCount */,
353                 0 /*metaState*/, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, 0 /*flags*/,
354                 inputSource);
355         event.setDisplayId(displayId);
356 
357         injectKeyEvent(event);
358         if (longpress) {
359             sleep(ViewConfiguration.getLongPressTimeout());
360             // Some long press behavior would check the event time, we set a new event time here.
361             final long nextEventTime = now + ViewConfiguration.getLongPressTimeout();
362             injectKeyEvent(KeyEvent.changeTimeRepeat(event, nextEventTime, 1 /* repeatCount */,
363                     KeyEvent.FLAG_LONG_PRESS));
364         }
365         injectKeyEvent(KeyEvent.changeAction(event, KeyEvent.ACTION_UP));
366     }
367 
sendKeyDoubleTap(int inputSource, int keyCode, int displayId)368     private void sendKeyDoubleTap(int inputSource, int keyCode, int displayId) {
369         sendKeyEvent(inputSource, keyCode, false, displayId);
370         sleep(ViewConfiguration.getDoubleTapMinTime());
371         sendKeyEvent(inputSource, keyCode, false, displayId);
372     }
373 
runTap(int inputSource, int displayId)374     private void runTap(int inputSource, int displayId) {
375         inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
376         sendTap(inputSource, Float.parseFloat(getNextArgRequired()),
377                 Float.parseFloat(getNextArgRequired()), displayId);
378     }
379 
sendTap(int inputSource, float x, float y, int displayId)380     private void sendTap(int inputSource, float x, float y, int displayId) {
381         final long now = SystemClock.uptimeMillis();
382         injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, now, x, y, 1.0f,
383                 displayId);
384         injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, now, x, y, 0.0f, displayId);
385     }
386 
runPress(int inputSource, int displayId)387     private void runPress(int inputSource, int displayId) {
388         inputSource = getSource(inputSource, InputDevice.SOURCE_TRACKBALL);
389         sendTap(inputSource, 0.0f, 0.0f, displayId);
390     }
391 
runSwipe(int inputSource, int displayId)392     private void runSwipe(int inputSource, int displayId) {
393         inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
394         sendSwipe(inputSource, displayId, false);
395     }
396 
sendSwipe(int inputSource, int displayId, boolean isDragDrop)397     private void sendSwipe(int inputSource, int displayId, boolean isDragDrop) {
398         // Parse two points and duration.
399         final float x1 = Float.parseFloat(getNextArgRequired());
400         final float y1 = Float.parseFloat(getNextArgRequired());
401         final float x2 = Float.parseFloat(getNextArgRequired());
402         final float y2 = Float.parseFloat(getNextArgRequired());
403         String durationArg = getNextArg();
404         int duration = durationArg != null ? Integer.parseInt(durationArg) : -1;
405         if (duration < 0) {
406             duration = 300;
407         }
408 
409         final long down = SystemClock.uptimeMillis();
410         injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, down, down, x1, y1, 1.0f,
411                 displayId);
412         if (isDragDrop) {
413             // long press until drag start.
414             sleep(ViewConfiguration.getLongPressTimeout());
415         }
416         long now = SystemClock.uptimeMillis();
417         final long endTime = down + duration;
418         while (now < endTime) {
419             final long elapsedTime = now - down;
420             final float alpha = (float) elapsedTime / duration;
421             injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, down, now,
422                     lerp(x1, x2, alpha), lerp(y1, y2, alpha), 1.0f, displayId);
423             now = SystemClock.uptimeMillis();
424         }
425         injectMotionEvent(inputSource, MotionEvent.ACTION_UP, down, now, x2, y2, 0.0f,
426                 displayId);
427     }
428 
runDragAndDrop(int inputSource, int displayId)429     private void runDragAndDrop(int inputSource, int displayId) {
430         inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
431         sendSwipe(inputSource, displayId, true);
432     }
433 
runRoll(int inputSource, int displayId)434     private void runRoll(int inputSource, int displayId) {
435         inputSource = getSource(inputSource, InputDevice.SOURCE_TRACKBALL);
436         sendMove(inputSource, Float.parseFloat(getNextArgRequired()),
437                 Float.parseFloat(getNextArgRequired()), displayId);
438     }
439 
440     /**
441      * Sends a simple zero-pressure move event.
442      *
443      * @param inputSource the InputDevice.SOURCE_* sending the input event
444      * @param dx change in x coordinate due to move
445      * @param dy change in y coordinate due to move
446      */
sendMove(int inputSource, float dx, float dy, int displayId)447     private void sendMove(int inputSource, float dx, float dy, int displayId) {
448         final long now = SystemClock.uptimeMillis();
449         injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, now, dx, dy, 0.0f,
450                 displayId);
451     }
452 
getAction()453     private int getAction() {
454         String actionString = getNextArgRequired();
455         switch (actionString.toUpperCase()) {
456             case "DOWN":
457                 return MotionEvent.ACTION_DOWN;
458             case "UP":
459                 return MotionEvent.ACTION_UP;
460             case "MOVE":
461                 return MotionEvent.ACTION_MOVE;
462             case "CANCEL":
463                 return MotionEvent.ACTION_CANCEL;
464             default:
465                 throw new IllegalArgumentException("Unknown action: " + actionString);
466         }
467     }
468 
runMotionEvent(int inputSource, int displayId)469     private void runMotionEvent(int inputSource, int displayId) {
470         inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
471         int action = getAction();
472         float x = 0, y = 0;
473         if (action == MotionEvent.ACTION_DOWN
474                 || action == MotionEvent.ACTION_MOVE
475                 || action == MotionEvent.ACTION_UP) {
476             x = Float.parseFloat(getNextArgRequired());
477             y = Float.parseFloat(getNextArgRequired());
478         } else {
479             // For ACTION_CANCEL, the positions are optional
480             String xString = getNextArg();
481             String yString = getNextArg();
482             if (xString != null && yString != null) {
483                 x = Float.parseFloat(xString);
484                 y = Float.parseFloat(yString);
485             }
486         }
487 
488         sendMotionEvent(inputSource, action, x, y, displayId);
489     }
490 
sendMotionEvent(int inputSource, int action, float x, float y, int displayId)491     private void sendMotionEvent(int inputSource, int action, float x, float y,
492             int displayId) {
493         float pressure = NO_PRESSURE;
494 
495         if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
496             pressure = DEFAULT_PRESSURE;
497         }
498 
499         final long now = SystemClock.uptimeMillis();
500         injectMotionEvent(inputSource, action, now, now, x, y, pressure, displayId);
501     }
502 
runKeyCombination(int inputSource, int displayId)503     private void runKeyCombination(int inputSource, int displayId) {
504         String arg = getNextArgRequired();
505 
506         // Get duration (optional).
507         long duration = 0;
508         if ("-t".equals(arg)) {
509             arg = getNextArgRequired();
510             duration = Integer.parseInt(arg);
511             arg = getNextArgRequired();
512         }
513 
514         IntArray keyCodes = new IntArray();
515         while (arg != null) {
516             final int keyCode = KeyEvent.keyCodeFromString(arg);
517             if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
518                 throw new IllegalArgumentException("Unknown keycode: " + arg);
519             }
520             keyCodes.add(keyCode);
521             arg = getNextArg();
522         }
523 
524         // At least 2 keys.
525         if (keyCodes.size() < 2) {
526             throw new IllegalArgumentException("keycombination requires at least 2 keycodes");
527         }
528 
529         sendKeyCombination(inputSource, keyCodes, displayId, duration);
530     }
531 
injectKeyEventAsync(KeyEvent event)532     private void injectKeyEventAsync(KeyEvent event) {
533         InputManager.getInstance().injectInputEvent(event,
534                 InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
535     }
536 
sendKeyCombination(int inputSource, IntArray keyCodes, int displayId, long duration)537     private void sendKeyCombination(int inputSource, IntArray keyCodes, int displayId,
538             long duration) {
539         final long now = SystemClock.uptimeMillis();
540         final int count = keyCodes.size();
541         final KeyEvent[] events = new KeyEvent[count];
542         int metaState = 0;
543         for (int i = 0; i < count; i++) {
544             final int keyCode = keyCodes.get(i);
545             final KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0,
546                     metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, 0 /*flags*/,
547                     inputSource);
548             event.setDisplayId(displayId);
549             events[i] = event;
550             // The order is important here, metaState could be updated and applied to the next key.
551             metaState |= MODIFIER.getOrDefault(keyCode, 0);
552         }
553 
554         for (KeyEvent event: events) {
555             // Use async inject so interceptKeyBeforeQueueing or interceptKeyBeforeDispatching could
556             // handle keys.
557             injectKeyEventAsync(event);
558         }
559 
560         sleep(duration);
561 
562         for (KeyEvent event: events) {
563             final int keyCode = event.getKeyCode();
564             final KeyEvent upEvent = new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode,
565                     0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, 0 /*flags*/,
566                     inputSource);
567             injectKeyEventAsync(upEvent);
568             metaState &= ~MODIFIER.getOrDefault(keyCode, 0);
569         }
570     }
571 
572     /**
573      * Puts the thread to sleep for the provided time.
574      *
575      * @param milliseconds The time to sleep in milliseconds.
576      */
sleep(long milliseconds)577     private void sleep(long milliseconds) {
578         try {
579             Thread.sleep(milliseconds);
580         } catch (InterruptedException e) {
581             throw new RuntimeException(e);
582         }
583     }
584 }
585