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