Adds duration args to allow arbitrary duration of keyevent press

This will be helpful to debug longer duration key press. This has
also been one of the requested features asked in stackoverflows (some
people tried to seed events directly to evdev as workaround).

New commands:
keyevent --duration <duration_ms> <keycode>

This will be mutually exclusive with --longpress, meaning that only one
of them can be passed. This 'duration_ms' will also be applied when
combined with other args ('--doubletap', '--delay').

Bug: 335533324
Test: atest InputShellCommandTest
Test: adb shell input keyevent --duration 1000

Change-Id: I3a86f5c4b89b435bc984b09ba6f504a59b5e0918
diff --git a/services/core/java/com/android/server/input/InputShellCommand.java b/services/core/java/com/android/server/input/InputShellCommand.java
index 138186b..4c5a3c2 100644
--- a/services/core/java/com/android/server/input/InputShellCommand.java
+++ b/services/core/java/com/android/server/input/InputShellCommand.java
@@ -333,8 +333,8 @@
             out.println();
             out.println("The commands and default sources are:");
             out.println("      text <string> (Default: keyboard)");
-            out.println("      keyevent [--longpress|--doubletap|--async"
-                    + "|--delay <duration between keycodes in ms>]"
+            out.println("      keyevent [--longpress|--duration <duration to hold key down in ms>]"
+                    + " [--doubletap] [--async] [--delay <duration between keycodes in ms>]"
                     + " <key code number or name> ..."
                     + " (Default: keyboard)");
             out.println("      tap <x> <y> (Default: touchscreen)");
@@ -402,6 +402,7 @@
         boolean async = false;
         boolean doubleTap = false;
         long delayMs = 0;
+        long durationMs = 0;
 
         String arg = getNextArgRequired();
         do {
@@ -411,9 +412,21 @@
             doubleTap = (doubleTap || arg.equals("--doubletap"));
             if (arg.equals("--delay")) {
                 delayMs = Long.parseLong(getNextArgRequired());
+            } else if (arg.equals("--duration")) {
+                durationMs = Long.parseLong(getNextArgRequired());
             }
         } while ((arg = getNextArg()) != null);
 
+        if (durationMs > 0 && longPress) {
+            getErrPrintWriter().println(
+                    "--duration and --longpress cannot be used at the same time.");
+            throw new IllegalArgumentException(
+                    "keyevent args should only contain either durationMs or longPress");
+        }
+        if (longPress) {
+            durationMs = ViewConfiguration.getLongPressTimeout();
+        }
+
         boolean firstInput = true;
         do {
             if (!firstInput && delayMs > 0) {
@@ -422,16 +435,17 @@
             firstInput = false;
 
             final int keyCode = KeyEvent.keyCodeFromString(arg);
-            sendKeyEvent(inputSource, keyCode, longPress, displayId, async);
+            sendKeyEvent(inputSource, keyCode, durationMs, displayId, async);
             if (doubleTap) {
                 sleep(ViewConfiguration.getDoubleTapMinTime());
-                sendKeyEvent(inputSource, keyCode, longPress, displayId, async);
+                sendKeyEvent(inputSource, keyCode, durationMs, displayId, async);
             }
         } while ((arg = getNextArg()) != null);
     }
 
     private void sendKeyEvent(
-            int inputSource, int keyCode, boolean longPress, int displayId, boolean async) {
+            int inputSource, int keyCode, long durationMs, int displayId,
+            boolean async) {
         final long now = SystemClock.uptimeMillis();
 
         KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0 /* repeatCount */,
@@ -440,13 +454,23 @@
         event.setDisplayId(displayId);
 
         injectKeyEvent(event, async);
-        if (longPress) {
-            sleep(ViewConfiguration.getLongPressTimeout());
-            // Some long press behavior would check the event time, we set a new event time here.
-            final long nextEventTime = now + ViewConfiguration.getLongPressTimeout();
-            KeyEvent longPressEvent = KeyEvent.changeTimeRepeat(
-                    event, nextEventTime, 1 /* repeatCount */, KeyEvent.FLAG_LONG_PRESS);
-            injectKeyEvent(longPressEvent, async);
+        long firstSleepDurationMs = Math.min(durationMs, ViewConfiguration.getLongPressTimeout());
+        if (firstSleepDurationMs > 0) {
+            sleep(firstSleepDurationMs);
+            // Send FLAG_LONG_PRESS right after `longPressTimeout`, and resume sleep if needed.
+            if (durationMs >= ViewConfiguration.getLongPressTimeout()) {
+                // Some long press behavior would check the event time, we set a new event time
+                // here.
+                final long nextEventTime = now + ViewConfiguration.getLongPressTimeout();
+                KeyEvent longPressEvent = KeyEvent.changeTimeRepeat(event, nextEventTime,
+                        1 /* repeatCount */, KeyEvent.FLAG_LONG_PRESS);
+                injectKeyEvent(longPressEvent, async);
+
+                long secondSleepDurationMs = durationMs - firstSleepDurationMs;
+                if (secondSleepDurationMs > 0) {
+                    sleep(secondSleepDurationMs);
+                }
+            }
         }
         injectKeyEvent(KeyEvent.changeAction(event, KeyEvent.ACTION_UP), async);
     }
diff --git a/tests/Input/src/com/android/server/input/InputShellCommandTest.java b/tests/Input/src/com/android/server/input/InputShellCommandTest.java
index f4845a5..11f4633 100644
--- a/tests/Input/src/com/android/server/input/InputShellCommandTest.java
+++ b/tests/Input/src/com/android/server/input/InputShellCommandTest.java
@@ -125,6 +125,14 @@
         assertThat(mInputEventInjector.mInjectedEvents).isEmpty();
     }
 
+    @Test
+    public void testInvalidKeyEventCommandArgsCombination() {
+        // --duration and --longpress must not be sent together
+        runCommand("keyevent --duration 1000 --longpress KEYCODE_A");
+
+        assertThat(mInputEventInjector.mInjectedEvents).isEmpty();
+    }
+
     private InputEvent getSingleInjectedInputEvent() {
         assertThat(mInputEventInjector.mInjectedEvents).hasSize(1);
         return mInputEventInjector.mInjectedEvents.get(0);