Accelerate input commands execution

Add input commands handler in input manager service, to support all
existing input commands from shell.
Redirect the java binary input to execute input manager service command
hander to accelerate the execution.
With the change the adb input command runs much faster. "input tap <x>
<y>" takes less than 10 ms from 500ms to run.

Bug: 132724511
Test: adb input tap <x> <y>, adb input swipe <x1> <y1> <x2> <y2>
Change-Id: I7d7a41627754e3471b110460e51f777322dfdb97
diff --git a/cmds/input/Android.bp b/cmds/input/Android.bp
index a0ebde6..1ee9dd3 100644
--- a/cmds/input/Android.bp
+++ b/cmds/input/Android.bp
@@ -1,8 +1,7 @@
 // Copyright 2008 The Android Open Source Project
 //
 
-java_binary {
+sh_binary {
     name: "input",
-    wrapper: "input",
-    srcs: ["**/*.java"],
+    src: "input",
 }
diff --git a/cmds/input/input b/cmds/input/input
index 2625eba..d7d0414 100755
--- a/cmds/input/input
+++ b/cmds/input/input
@@ -1,3 +1,2 @@
 #!/system/bin/sh
-export CLASSPATH=/system/framework/input.jar
-exec app_process /system/bin com.android.commands.input.Input "$@"
+cmd input "$@"
diff --git a/cmds/input/src/com/android/commands/input/Input.java b/cmds/input/src/com/android/commands/input/Input.java
deleted file mode 100644
index 08216d9..0000000
--- a/cmds/input/src/com/android/commands/input/Input.java
+++ /dev/null
@@ -1,435 +0,0 @@
-/*
- * Copyright (C) 2007 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.commands.input;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.Display.INVALID_DISPLAY;
-
-import android.hardware.input.InputManager;
-import android.os.SystemClock;
-import android.view.InputDevice;
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.ViewConfiguration;
-
-import com.android.internal.os.BaseCommand;
-
-import java.io.PrintStream;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Command that sends key events to the device, either by their keycode, or by
- * desired character output.
- */
-
-public class Input extends BaseCommand {
-    private static final String TAG = "Input";
-    private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
-    private static final String INVALID_DISPLAY_ARGUMENTS =
-            "Error: Invalid arguments for display ID.";
-
-    private static final float DEFAULT_PRESSURE = 1.0f;
-    private static final float NO_PRESSURE = 0.0f;
-
-    private static final Map<String, Integer> SOURCES = new HashMap<String, Integer>() {{
-        put("keyboard", InputDevice.SOURCE_KEYBOARD);
-        put("dpad", InputDevice.SOURCE_DPAD);
-        put("gamepad", InputDevice.SOURCE_GAMEPAD);
-        put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
-        put("mouse", InputDevice.SOURCE_MOUSE);
-        put("stylus", InputDevice.SOURCE_STYLUS);
-        put("trackball", InputDevice.SOURCE_TRACKBALL);
-        put("touchpad", InputDevice.SOURCE_TOUCHPAD);
-        put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
-        put("joystick", InputDevice.SOURCE_JOYSTICK);
-    }};
-
-    private static final Map<String, InputCmd> COMMANDS = new HashMap<String, InputCmd>();
-
-    /**
-     * Command-line entry point.
-     *
-     * @param args The command-line arguments
-     */
-    public static void main(String[] args) {
-        (new Input()).run(args);
-    }
-
-    Input() {
-        COMMANDS.put("text", new InputText());
-        COMMANDS.put("keyevent", new InputKeyEvent());
-        COMMANDS.put("tap", new InputTap());
-        COMMANDS.put("swipe", new InputSwipe());
-        COMMANDS.put("draganddrop", new InputDragAndDrop());
-        COMMANDS.put("press", new InputPress());
-        COMMANDS.put("roll", new InputRoll());
-        COMMANDS.put("motionevent", new InputMotionEvent());
-    }
-
-    @Override
-    public void onRun() throws Exception {
-        String arg = nextArgRequired();
-        int inputSource = InputDevice.SOURCE_UNKNOWN;
-
-        // Get source (optional).
-        if (SOURCES.containsKey(arg)) {
-            inputSource = SOURCES.get(arg);
-            arg = nextArgRequired();
-        }
-
-        // Get displayId (optional).
-        int displayId = INVALID_DISPLAY;
-        if ("-d".equals(arg)) {
-            displayId = getDisplayId();
-            arg = nextArgRequired();
-        }
-
-        // Get command and run.
-        InputCmd cmd = COMMANDS.get(arg);
-        if (cmd != null) {
-            try {
-                cmd.run(inputSource, displayId);
-                return;
-            } catch (NumberFormatException ex) {
-                throw new IllegalArgumentException(INVALID_ARGUMENTS + arg);
-            }
-        }
-
-        throw new IllegalArgumentException("Error: Unknown command: " + arg);
-    }
-
-    private int getDisplayId() {
-        String displayArg = nextArgRequired();
-        if ("INVALID_DISPLAY".equalsIgnoreCase(displayArg)) {
-            return INVALID_DISPLAY;
-        } else if ("DEFAULT_DISPLAY".equalsIgnoreCase(displayArg)) {
-            return DEFAULT_DISPLAY;
-        } else {
-            try {
-                final int displayId = Integer.parseInt(displayArg);
-                if (displayId == INVALID_DISPLAY) {
-                    return INVALID_DISPLAY;
-                }
-                return Math.max(displayId, 0);
-            } catch (NumberFormatException e) {
-                throw new IllegalArgumentException(INVALID_DISPLAY_ARGUMENTS);
-            }
-        }
-    }
-
-    class InputText implements InputCmd {
-        @Override
-        public void run(int inputSource, int displayId) {
-            inputSource = getSource(inputSource, InputDevice.SOURCE_KEYBOARD);
-            sendText(inputSource, nextArgRequired(), displayId);
-        }
-
-        /**
-         * Convert the characters of string text into key event's and send to
-         * device.
-         *
-         * @param text is a string of characters you want to input to the device.
-         */
-        private void sendText(int source, final String text, int displayId) {
-            final StringBuffer buff = new StringBuffer(text);
-            boolean escapeFlag = false;
-            for (int i = 0; i < buff.length(); i++) {
-                if (escapeFlag) {
-                    escapeFlag = false;
-                    if (buff.charAt(i) == 's') {
-                        buff.setCharAt(i, ' ');
-                        buff.deleteCharAt(--i);
-                    }
-                }
-                if (buff.charAt(i) == '%') {
-                    escapeFlag = true;
-                }
-            }
-
-            final char[] chars = buff.toString().toCharArray();
-            final KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
-            final KeyEvent[] events = kcm.getEvents(chars);
-            for (int i = 0; i < events.length; i++) {
-                KeyEvent e = events[i];
-                if (source != e.getSource()) {
-                    e.setSource(source);
-                }
-                e.setDisplayId(displayId);
-                injectKeyEvent(e);
-            }
-        }
-    }
-
-    class InputKeyEvent implements InputCmd {
-        @Override
-        public void run(int inputSource, int displayId) {
-            String arg = nextArgRequired();
-            final boolean longpress = "--longpress".equals(arg);
-            if (longpress) {
-                arg = nextArgRequired();
-            }
-
-            do {
-                final int keycode = KeyEvent.keyCodeFromString(arg);
-                sendKeyEvent(inputSource, keycode, longpress, displayId);
-            } while ((arg = nextArg()) != null);
-        }
-
-        private void sendKeyEvent(int inputSource, int keyCode, boolean longpress, int displayId) {
-            final long now = SystemClock.uptimeMillis();
-            int repeatCount = 0;
-
-            KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, repeatCount,
-                    0 /*metaState*/, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, 0 /*flags*/,
-                    inputSource);
-            event.setDisplayId(displayId);
-
-            injectKeyEvent(event);
-            if (longpress) {
-                repeatCount++;
-                injectKeyEvent(KeyEvent.changeTimeRepeat(event, now, repeatCount,
-                        KeyEvent.FLAG_LONG_PRESS));
-            }
-            injectKeyEvent(KeyEvent.changeAction(event, KeyEvent.ACTION_UP));
-        }
-    }
-
-    class InputTap implements InputCmd {
-        @Override
-        public void run(int inputSource, int displayId) {
-            inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
-            sendTap(inputSource, Float.parseFloat(nextArgRequired()),
-                    Float.parseFloat(nextArgRequired()), displayId);
-        }
-
-        void sendTap(int inputSource, float x, float y, int displayId) {
-            final long now = SystemClock.uptimeMillis();
-            injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, now, x, y, 1.0f,
-                    displayId);
-            injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, now, x, y, 0.0f, displayId);
-        }
-    }
-
-    class InputPress extends InputTap {
-        @Override
-        public void run(int inputSource, int displayId) {
-            inputSource = getSource(inputSource, InputDevice.SOURCE_TRACKBALL);
-            sendTap(inputSource, 0.0f, 0.0f, displayId);
-        }
-    }
-
-    class InputSwipe implements InputCmd {
-        @Override
-        public void run(int inputSource, int displayId) {
-            inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
-            sendSwipe(inputSource, displayId, false);
-        }
-
-        void sendSwipe(int inputSource, int displayId, boolean isDragDrop) {
-            // Parse two points and duration.
-            final float x1 = Float.parseFloat(nextArgRequired());
-            final float y1 = Float.parseFloat(nextArgRequired());
-            final float x2 = Float.parseFloat(nextArgRequired());
-            final float y2 = Float.parseFloat(nextArgRequired());
-            String durationArg = nextArg();
-            int duration = durationArg != null ? Integer.parseInt(durationArg) : -1;
-            if (duration < 0) {
-                duration = 300;
-            }
-
-            final long down = SystemClock.uptimeMillis();
-            injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, down, down, x1, y1, 1.0f,
-                    displayId);
-            if (isDragDrop) {
-                // long press until drag start.
-                try {
-                    Thread.sleep(ViewConfiguration.getLongPressTimeout());
-                } catch (InterruptedException e) {
-                    throw new RuntimeException(e);
-                }
-            }
-            long now = SystemClock.uptimeMillis();
-            final long endTime = down + duration;
-            while (now < endTime) {
-                final long elapsedTime = now - down;
-                final float alpha = (float) elapsedTime / duration;
-                injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, down, now,
-                        lerp(x1, x2, alpha), lerp(y1, y2, alpha), 1.0f, displayId);
-                now = SystemClock.uptimeMillis();
-            }
-            injectMotionEvent(inputSource, MotionEvent.ACTION_UP, down, now, x2, y2, 0.0f,
-                    displayId);
-        }
-    }
-
-    class InputDragAndDrop extends InputSwipe {
-        @Override
-        public void run(int inputSource, int displayId) {
-            inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
-            sendSwipe(inputSource, displayId, true);
-        }
-    }
-
-    class InputRoll implements InputCmd {
-        @Override
-        public void run(int inputSource, int displayId) {
-            inputSource = getSource(inputSource, InputDevice.SOURCE_TRACKBALL);
-            sendMove(inputSource, Float.parseFloat(nextArgRequired()),
-                    Float.parseFloat(nextArgRequired()), displayId);
-        }
-
-        /**
-         * Sends a simple zero-pressure move event.
-         *
-         * @param inputSource the InputDevice.SOURCE_* sending the input event
-         * @param dx change in x coordinate due to move
-         * @param dy change in y coordinate due to move
-         */
-        private void sendMove(int inputSource, float dx, float dy, int displayId) {
-            final long now = SystemClock.uptimeMillis();
-            injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, now, dx, dy, 0.0f,
-                    displayId);
-        }
-    }
-
-    class InputMotionEvent implements InputCmd {
-        @Override
-        public void run(int inputSource, int displayId) {
-            inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
-            sendMotionEvent(inputSource, nextArgRequired(), Float.parseFloat(nextArgRequired()),
-                    Float.parseFloat(nextArgRequired()), displayId);
-        }
-
-        private void sendMotionEvent(int inputSource, String motionEventType, float x, float y,
-                int displayId) {
-            final int action;
-            final float pressure;
-
-            switch (motionEventType.toUpperCase()) {
-                case "DOWN":
-                    action = MotionEvent.ACTION_DOWN;
-                    pressure = DEFAULT_PRESSURE;
-                    break;
-                case "UP":
-                    action = MotionEvent.ACTION_UP;
-                    pressure = NO_PRESSURE;
-                    break;
-                case "MOVE":
-                    action = MotionEvent.ACTION_MOVE;
-                    pressure = DEFAULT_PRESSURE;
-                    break;
-                default:
-                    throw new IllegalArgumentException("Unknown motionevent " + motionEventType);
-            }
-
-            final long now = SystemClock.uptimeMillis();
-            injectMotionEvent(inputSource, action, now, now, x, y, pressure, displayId);
-        }
-    }
-
-    /**
-     * Abstract class for command
-     * use nextArgRequired or nextArg to check next argument if necessary.
-     */
-    private interface InputCmd {
-        void run(int inputSource, int displayId);
-    }
-
-    private static void injectKeyEvent(KeyEvent event) {
-        InputManager.getInstance().injectInputEvent(event,
-                InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
-    }
-
-    private static int getInputDeviceId(int inputSource) {
-        final int DEFAULT_DEVICE_ID = 0;
-        int[] devIds = InputDevice.getDeviceIds();
-        for (int devId : devIds) {
-            InputDevice inputDev = InputDevice.getDevice(devId);
-            if (inputDev.supportsSource(inputSource)) {
-                return devId;
-            }
-        }
-        return DEFAULT_DEVICE_ID;
-    }
-
-    /**
-     * Builds a MotionEvent and injects it into the event stream.
-     *
-     * @param inputSource the InputDevice.SOURCE_* sending the input event
-     * @param action the MotionEvent.ACTION_* for the event
-     * @param downTime the value of the ACTION_DOWN event happened
-     * @param when the value of SystemClock.uptimeMillis() at which the event happened
-     * @param x x coordinate of event
-     * @param y y coordinate of event
-     * @param pressure pressure of event
-     */
-    private static void injectMotionEvent(int inputSource, int action, long downTime, long when,
-            float x, float y, float pressure, int displayId) {
-        final float DEFAULT_SIZE = 1.0f;
-        final int DEFAULT_META_STATE = 0;
-        final float DEFAULT_PRECISION_X = 1.0f;
-        final float DEFAULT_PRECISION_Y = 1.0f;
-        final int DEFAULT_EDGE_FLAGS = 0;
-        MotionEvent event = MotionEvent.obtain(downTime, when, action, x, y, pressure, DEFAULT_SIZE,
-                DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y,
-                getInputDeviceId(inputSource), DEFAULT_EDGE_FLAGS);
-        event.setSource(inputSource);
-        if (displayId == INVALID_DISPLAY && (inputSource & InputDevice.SOURCE_CLASS_POINTER) != 0) {
-            displayId = DEFAULT_DISPLAY;
-        }
-        event.setDisplayId(displayId);
-        InputManager.getInstance().injectInputEvent(event,
-                InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
-    }
-
-    private static final float lerp(float a, float b, float alpha) {
-        return (b - a) * alpha + a;
-    }
-
-    private static final int getSource(int inputSource, int defaultSource) {
-        return inputSource == InputDevice.SOURCE_UNKNOWN ? defaultSource : inputSource;
-    }
-
-    @Override
-    public void onShowUsage(PrintStream out) {
-        out.println("Usage: input [<source>] [-d DISPLAY_ID] <command> [<arg>...]");
-        out.println();
-        out.println("The sources are: ");
-        for (String src : SOURCES.keySet()) {
-            out.println("      " + src);
-        }
-        out.println();
-        out.printf("-d: specify the display ID.\n"
-                + "      (Default: %d for key event, %d for motion event if not specified.)",
-                INVALID_DISPLAY, DEFAULT_DISPLAY);
-        out.println();
-        out.println("The commands and default sources are:");
-        out.println("      text <string> (Default: touchscreen)");
-        out.println("      keyevent [--longpress] <key code number or name> ..."
-                + " (Default: keyboard)");
-        out.println("      tap <x> <y> (Default: touchscreen)");
-        out.println("      swipe <x1> <y1> <x2> <y2> [duration(ms)]"
-                + " (Default: touchscreen)");
-        out.println("      draganddrop <x1> <y1> <x2> <y2> [duration(ms)]"
-                + " (Default: touchscreen)");
-        out.println("      press (Default: trackball)");
-        out.println("      roll <dx> <dy> (Default: trackball)");
-        out.println("      motionevent <DOWN|UP|MOVE> <x> <y> (Default: touchscreen)");
-    }
-}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 115899a..d9b25ba 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -59,6 +59,8 @@
 import android.os.MessageQueue;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
@@ -115,6 +117,7 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
+
 /*
  * Wraps the C++ InputManager and provides its callbacks.
  */
@@ -2506,4 +2509,11 @@
             return InputManagerService.this.transferTouchFocus(fromChannelToken, toChannelToken);
         }
     }
+
+    @Override
+    public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
+            String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
+        new InputShellCommand().exec(this, in, out, err, args, callback, resultReceiver);
+    }
+
 }
diff --git a/services/core/java/com/android/server/input/InputShellCommand.java b/services/core/java/com/android/server/input/InputShellCommand.java
new file mode 100644
index 0000000..533392a
--- /dev/null
+++ b/services/core/java/com/android/server/input/InputShellCommand.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2007 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.server.input;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+
+import android.hardware.input.InputManager;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Command that sends input events to the device.
+ */
+
+public class InputShellCommand extends ShellCommand {
+    private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
+    private static final String INVALID_DISPLAY_ARGUMENTS =
+            "Error: Invalid arguments for display ID.";
+    private static final int DEFAULT_DEVICE_ID = 0;
+    private static final float DEFAULT_PRESSURE = 1.0f;
+    private static final float NO_PRESSURE = 0.0f;
+    private static final float DEFAULT_SIZE = 1.0f;
+    private static final int DEFAULT_META_STATE = 0;
+    private static final float DEFAULT_PRECISION_X = 1.0f;
+    private static final float DEFAULT_PRECISION_Y = 1.0f;
+    private static final int DEFAULT_EDGE_FLAGS = 0;
+
+    private static final Map<String, Integer> SOURCES = new HashMap<String, Integer>() {{
+            put("keyboard", InputDevice.SOURCE_KEYBOARD);
+            put("dpad", InputDevice.SOURCE_DPAD);
+            put("gamepad", InputDevice.SOURCE_GAMEPAD);
+            put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
+            put("mouse", InputDevice.SOURCE_MOUSE);
+            put("stylus", InputDevice.SOURCE_STYLUS);
+            put("trackball", InputDevice.SOURCE_TRACKBALL);
+            put("touchpad", InputDevice.SOURCE_TOUCHPAD);
+            put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
+            put("joystick", InputDevice.SOURCE_JOYSTICK);
+        }};
+
+    private void injectKeyEvent(KeyEvent event) {
+        InputManager.getInstance().injectInputEvent(event,
+                InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
+    }
+
+    private int getInputDeviceId(int inputSource) {
+        int[] devIds = InputDevice.getDeviceIds();
+        for (int devId : devIds) {
+            InputDevice inputDev = InputDevice.getDevice(devId);
+            if (inputDev.supportsSource(inputSource)) {
+                return devId;
+            }
+        }
+        return DEFAULT_DEVICE_ID;
+    }
+
+    private int getDisplayId() {
+        String displayArg = getNextArgRequired();
+        if ("INVALID_DISPLAY".equalsIgnoreCase(displayArg)) {
+            return INVALID_DISPLAY;
+        } else if ("DEFAULT_DISPLAY".equalsIgnoreCase(displayArg)) {
+            return DEFAULT_DISPLAY;
+        } else {
+            try {
+                final int displayId = Integer.parseInt(displayArg);
+                if (displayId == INVALID_DISPLAY) {
+                    return INVALID_DISPLAY;
+                }
+                return Math.max(displayId, 0);
+            } catch (NumberFormatException e) {
+                throw new IllegalArgumentException(INVALID_DISPLAY_ARGUMENTS);
+            }
+        }
+    }
+
+    /**
+     * Builds a MotionEvent and injects it into the event stream.
+     *
+     * @param inputSource the InputDevice.SOURCE_* sending the input event
+     * @param action the MotionEvent.ACTION_* for the event
+     * @param downTime the value of the ACTION_DOWN event happened
+     * @param when the value of SystemClock.uptimeMillis() at which the event happened
+     * @param x x coordinate of event
+     * @param y y coordinate of event
+     * @param pressure pressure of event
+     */
+    private void injectMotionEvent(int inputSource, int action, long downTime, long when,
+            float x, float y, float pressure, int displayId) {
+        MotionEvent event = MotionEvent.obtain(downTime, when, action, x, y, pressure,
+                DEFAULT_SIZE, DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y,
+                getInputDeviceId(inputSource), DEFAULT_EDGE_FLAGS);
+        event.setSource(inputSource);
+        if (displayId == INVALID_DISPLAY
+                && (inputSource & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            displayId = DEFAULT_DISPLAY;
+        }
+        event.setDisplayId(displayId);
+        InputManager.getInstance().injectInputEvent(event,
+                InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
+    }
+
+    private float lerp(float a, float b, float alpha) {
+        return (b - a) * alpha + a;
+    }
+
+    private int getSource(int inputSource, int defaultSource) {
+        return inputSource == InputDevice.SOURCE_UNKNOWN ? defaultSource : inputSource;
+    }
+
+    @Override
+    public final int onCommand(String cmd) {
+        String arg = cmd;
+        int inputSource = InputDevice.SOURCE_UNKNOWN;
+        // Get source (optional).
+        if (SOURCES.containsKey(arg)) {
+            inputSource = SOURCES.get(arg);
+            arg = getNextArgRequired();
+        }
+
+        // Get displayId (optional).
+        int displayId = INVALID_DISPLAY;
+        if ("-d".equals(arg)) {
+            displayId = getDisplayId();
+            arg = getNextArgRequired();
+        }
+
+        try {
+            if ("text".equals(arg)) {
+                runText(inputSource, displayId);
+            } else if ("keyevent".equals(arg)) {
+                runKeyEvent(inputSource, displayId);
+            } else if ("tap".equals(arg)) {
+                runTap(inputSource, displayId);
+            } else if ("swipe".equals(arg)) {
+                runSwipe(inputSource, displayId);
+            } else if ("draganddrop".equals(arg)) {
+                runDragAndDrop(inputSource, displayId);
+            } else if ("press".equals(arg)) {
+                runPress(inputSource, displayId);
+            } else if ("roll".equals(arg)) {
+                runRoll(inputSource, displayId);
+            }  else if ("motionevent".equals(arg)) {
+                runMotionEvent(inputSource, displayId);
+            } else {
+                handleDefaultCommands(arg);
+            }
+        } catch (NumberFormatException ex) {
+            throw new IllegalArgumentException(INVALID_ARGUMENTS + arg);
+        }
+        return 0;
+    }
+
+    @Override
+    public final void onHelp() {
+        try (PrintWriter out = getOutPrintWriter();) {
+            out.println("Usage: input [<source>] [-d DISPLAY_ID] <command> [<arg>...]");
+            out.println();
+            out.println("The sources are: ");
+            for (String src : SOURCES.keySet()) {
+                out.println("      " + src);
+            }
+            out.println();
+            out.printf("-d: specify the display ID.\n      (Default: %d for key event, "
+                    + "%d for motion event if not specified.)",
+                    INVALID_DISPLAY, DEFAULT_DISPLAY);
+            out.println();
+            out.println("The commands and default sources are:");
+            out.println("      text <string> (Default: touchscreen)");
+            out.println("      keyevent [--longpress] <key code number or name> ..."
+                    + " (Default: keyboard)");
+            out.println("      tap <x> <y> (Default: touchscreen)");
+            out.println("      swipe <x1> <y1> <x2> <y2> [duration(ms)]"
+                    + " (Default: touchscreen)");
+            out.println("      draganddrop <x1> <y1> <x2> <y2> [duration(ms)]"
+                    + " (Default: touchscreen)");
+            out.println("      press (Default: trackball)");
+            out.println("      roll <dx> <dy> (Default: trackball)");
+            out.println("      motionevent <DOWN|UP|MOVE> <x> <y> (Default: touchscreen)");
+        }
+    }
+
+    private void runText(int inputSource, int displayId) {
+        inputSource = getSource(inputSource, InputDevice.SOURCE_KEYBOARD);
+        sendText(inputSource, getNextArgRequired(), displayId);
+    }
+
+    /**
+     * Convert the characters of string text into key event's and send to
+     * device.
+     *
+     * @param text is a string of characters you want to input to the device.
+     */
+    private void sendText(int source, final String text, int displayId) {
+        final StringBuffer buff = new StringBuffer(text);
+        boolean escapeFlag = false;
+        for (int i = 0; i < buff.length(); i++) {
+            if (escapeFlag) {
+                escapeFlag = false;
+                if (buff.charAt(i) == 's') {
+                    buff.setCharAt(i, ' ');
+                    buff.deleteCharAt(--i);
+                }
+            }
+            if (buff.charAt(i) == '%') {
+                escapeFlag = true;
+            }
+        }
+
+        final char[] chars = buff.toString().toCharArray();
+        final KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+        final KeyEvent[] events = kcm.getEvents(chars);
+        for (int i = 0; i < events.length; i++) {
+            KeyEvent e = events[i];
+            if (source != e.getSource()) {
+                e.setSource(source);
+            }
+            e.setDisplayId(displayId);
+            injectKeyEvent(e);
+        }
+    }
+
+    private void runKeyEvent(int inputSource, int displayId) {
+        String arg = getNextArgRequired();
+        final boolean longpress = "--longpress".equals(arg);
+        if (longpress) {
+            arg = getNextArgRequired();
+        }
+
+        do {
+            final int keycode = KeyEvent.keyCodeFromString(arg);
+            sendKeyEvent(inputSource, keycode, longpress, displayId);
+        } while ((arg = getNextArg()) != null);
+    }
+
+    private void sendKeyEvent(int inputSource, int keyCode, boolean longpress, int displayId) {
+        final long now = SystemClock.uptimeMillis();
+        int repeatCount = 0;
+
+        KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, repeatCount,
+                0 /*metaState*/, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, 0 /*flags*/,
+                inputSource);
+        event.setDisplayId(displayId);
+
+        injectKeyEvent(event);
+        if (longpress) {
+            repeatCount++;
+            injectKeyEvent(KeyEvent.changeTimeRepeat(event, now, repeatCount,
+                    KeyEvent.FLAG_LONG_PRESS));
+        }
+        injectKeyEvent(KeyEvent.changeAction(event, KeyEvent.ACTION_UP));
+    }
+
+    private void runTap(int inputSource, int displayId) {
+        inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
+        sendTap(inputSource, Float.parseFloat(getNextArgRequired()),
+                Float.parseFloat(getNextArgRequired()), displayId);
+    }
+
+    private void sendTap(int inputSource, float x, float y, int displayId) {
+        final long now = SystemClock.uptimeMillis();
+        injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, now, x, y, 1.0f,
+                displayId);
+        injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, now, x, y, 0.0f, displayId);
+    }
+
+    private void runPress(int inputSource, int displayId) {
+        inputSource = getSource(inputSource, InputDevice.SOURCE_TRACKBALL);
+        sendTap(inputSource, 0.0f, 0.0f, displayId);
+    }
+
+    private void runSwipe(int inputSource, int displayId) {
+        inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
+        sendSwipe(inputSource, displayId, false);
+    }
+
+    private void sendSwipe(int inputSource, int displayId, boolean isDragDrop) {
+        // Parse two points and duration.
+        final float x1 = Float.parseFloat(getNextArgRequired());
+        final float y1 = Float.parseFloat(getNextArgRequired());
+        final float x2 = Float.parseFloat(getNextArgRequired());
+        final float y2 = Float.parseFloat(getNextArgRequired());
+        String durationArg = getNextArg();
+        int duration = durationArg != null ? Integer.parseInt(durationArg) : -1;
+        if (duration < 0) {
+            duration = 300;
+        }
+
+        final long down = SystemClock.uptimeMillis();
+        injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, down, down, x1, y1, 1.0f,
+                displayId);
+        if (isDragDrop) {
+            // long press until drag start.
+            try {
+                Thread.sleep(ViewConfiguration.getLongPressTimeout());
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+        long now = SystemClock.uptimeMillis();
+        final long endTime = down + duration;
+        while (now < endTime) {
+            final long elapsedTime = now - down;
+            final float alpha = (float) elapsedTime / duration;
+            injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, down, now,
+                    lerp(x1, x2, alpha), lerp(y1, y2, alpha), 1.0f, displayId);
+            now = SystemClock.uptimeMillis();
+        }
+        injectMotionEvent(inputSource, MotionEvent.ACTION_UP, down, now, x2, y2, 0.0f,
+                displayId);
+    }
+
+    private void runDragAndDrop(int inputSource, int displayId) {
+        inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
+        sendSwipe(inputSource, displayId, true);
+    }
+
+    private void runRoll(int inputSource, int displayId) {
+        inputSource = getSource(inputSource, InputDevice.SOURCE_TRACKBALL);
+        sendMove(inputSource, Float.parseFloat(getNextArgRequired()),
+                Float.parseFloat(getNextArgRequired()), displayId);
+    }
+
+    /**
+     * Sends a simple zero-pressure move event.
+     *
+     * @param inputSource the InputDevice.SOURCE_* sending the input event
+     * @param dx change in x coordinate due to move
+     * @param dy change in y coordinate due to move
+     */
+    private void sendMove(int inputSource, float dx, float dy, int displayId) {
+        final long now = SystemClock.uptimeMillis();
+        injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, now, dx, dy, 0.0f,
+                displayId);
+    }
+
+    private void runMotionEvent(int inputSource, int displayId) {
+        inputSource = getSource(inputSource, InputDevice.SOURCE_TOUCHSCREEN);
+        sendMotionEvent(inputSource, getNextArgRequired(), Float.parseFloat(getNextArgRequired()),
+                Float.parseFloat(getNextArgRequired()), displayId);
+    }
+
+    private void sendMotionEvent(int inputSource, String motionEventType, float x, float y,
+            int displayId) {
+        final int action;
+        final float pressure;
+
+        switch (motionEventType.toUpperCase()) {
+            case "DOWN":
+                action = MotionEvent.ACTION_DOWN;
+                pressure = DEFAULT_PRESSURE;
+                break;
+            case "UP":
+                action = MotionEvent.ACTION_UP;
+                pressure = NO_PRESSURE;
+                break;
+            case "MOVE":
+                action = MotionEvent.ACTION_MOVE;
+                pressure = DEFAULT_PRESSURE;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown motionevent " + motionEventType);
+        }
+
+        final long now = SystemClock.uptimeMillis();
+        injectMotionEvent(inputSource, action, now, now, x, y, pressure, displayId);
+    }
+}