Merge "Adds duration args to allow arbitrary duration of keyevent press" into main
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);