Loading services/core/java/com/android/server/input/InputShellCommand.java +129 −8 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.input; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.InputDevice.SOURCE_CLASS_POINTER; import static android.view.KeyEvent.KEYCODE_ALT_LEFT; import static android.view.KeyEvent.KEYCODE_ALT_RIGHT; import static android.view.KeyEvent.KEYCODE_CTRL_LEFT; Loading @@ -38,6 +39,11 @@ import static android.view.KeyEvent.META_META_RIGHT_ON; import static android.view.KeyEvent.META_SHIFT_LEFT_ON; import static android.view.KeyEvent.META_SHIFT_ON; import static android.view.KeyEvent.META_SHIFT_RIGHT_ON; import static android.view.MotionEvent.AXIS_HSCROLL; import static android.view.MotionEvent.AXIS_SCROLL; import static android.view.MotionEvent.AXIS_VSCROLL; import static android.view.MotionEvent.AXIS_X; import static android.view.MotionEvent.AXIS_Y; import static java.util.Collections.unmodifiableMap; Loading @@ -47,14 +53,21 @@ import android.os.ShellCommand; import android.os.SystemClock; import android.util.ArrayMap; import android.util.IntArray; import android.util.Pair; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; /** * Command that sends input events to the device. Loading Loading @@ -107,15 +120,31 @@ public class InputShellCommand extends ShellCommand { map.put("touchpad", InputDevice.SOURCE_TOUCHPAD); map.put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION); map.put("joystick", InputDevice.SOURCE_JOYSTICK); map.put("rotaryencoder", InputDevice.SOURCE_ROTARY_ENCODER); SOURCES = unmodifiableMap(map); } public InputShellCommand() { this(InputShellCommand::injectInputEvent); } @VisibleForTesting InputShellCommand(BiConsumer<InputEvent, Integer> inputEventInjector) { mInputEventInjector = inputEventInjector;; } private static void injectInputEvent(InputEvent event, Integer injectMode) { InputManagerGlobal.getInstance().injectInputEvent(event, injectMode); } private final BiConsumer<InputEvent, Integer> mInputEventInjector; private void injectKeyEvent(KeyEvent event, boolean async) { int injectMode = async ? InputManager.INJECT_INPUT_EVENT_MODE_ASYNC : InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; InputManagerGlobal.getInstance().injectInputEvent(event, injectMode); mInputEventInjector.accept(event, injectMode); } private int getInputDeviceId(int inputSource) { Loading Loading @@ -161,19 +190,41 @@ public class InputShellCommand extends ShellCommand { */ private void injectMotionEvent(int inputSource, int action, long downTime, long when, float x, float y, float pressure, int displayId) { final Map<Integer, Float> axisValues = Map.of( MotionEvent.AXIS_X, x, MotionEvent.AXIS_Y, y, MotionEvent.AXIS_PRESSURE, pressure); injectMotionEvent(inputSource, action, downTime, when, axisValues, displayId); } /** * 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 axisValues a map of an axis to the respective axis value * @param displayId the ID of the display associated to the event */ private void injectMotionEvent(int inputSource, int action, long downTime, long when, Map<Integer, Float> axisValues, int displayId) { final int pointerCount = 1; MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[pointerCount]; MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount]; for (int i = 0; i < pointerCount; i++) { pointerProperties[i] = new MotionEvent.PointerProperties(); pointerProperties[i].id = i; pointerProperties[i].toolType = getToolType(inputSource); } MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount]; for (int i = 0; i < pointerCount; i++) { pointerCoords[i] = new MotionEvent.PointerCoords(); pointerCoords[i].x = x; pointerCoords[i].y = y; pointerCoords[i].pressure = pressure; pointerCoords[i].size = DEFAULT_SIZE; for (var entry : axisValues.entrySet()) { pointerCoords[i].setAxisValue(entry.getKey(), entry.getValue()); } } if (displayId == INVALID_DISPLAY && (inputSource & InputDevice.SOURCE_CLASS_POINTER) != 0) { Loading @@ -183,7 +234,7 @@ public class InputShellCommand extends ShellCommand { pointerProperties, pointerCoords, DEFAULT_META_STATE, DEFAULT_BUTTON_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, getInputDeviceId(inputSource), DEFAULT_EDGE_FLAGS, inputSource, displayId, DEFAULT_FLAGS); InputManagerGlobal.getInstance().injectInputEvent(event, mInputEventInjector.accept(event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); } Loading Loading @@ -246,6 +297,8 @@ public class InputShellCommand extends ShellCommand { runPress(inputSource, displayId); } else if ("roll".equals(arg)) { runRoll(inputSource, displayId); } else if ("scroll".equals(arg)) { runScroll(inputSource, displayId); } else if ("motionevent".equals(arg)) { runMotionEvent(inputSource, displayId); } else if ("keycombination".equals(arg)) { Loading @@ -268,13 +321,18 @@ public class InputShellCommand extends ShellCommand { for (String src : SOURCES.keySet()) { out.println(" " + src); } out.println("[axis_value] represents an option specifying the value of a given axis "); out.println(" The syntax is as follows: --axis <axis_name>,<axis_value>"); out.println(" where <axis_name> is the name of the axis as defined in "); out.println(" MotionEvent without the AXIS_ prefix (e.g. SCROLL, X)"); out.println(" Sample [axis_values] entry: `--axis Y,3`, `--axis SCROLL,-2`"); 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(" text <string> (Default: keyboard)"); out.println(" keyevent [--longpress|--doubletap|--async" + "|--delay <duration between keycodes in ms>]" + " <key code number or name> ..." Loading @@ -287,6 +345,13 @@ public class InputShellCommand extends ShellCommand { out.println(" press (Default: trackball)"); out.println(" roll <dx> <dy> (Default: trackball)"); out.println(" motionevent <DOWN|UP|MOVE|CANCEL> <x> <y> (Default: touchscreen)"); out.println(" scroll (Default: rotaryencoder). Has the following syntax:"); out.println(" scroll <x> <y> [axis_value] (for pointer-based sources)"); out.println(" scroll [axis_value] (for non-pointer-based sources)"); out.println(" Axis options: SCROLL, HSCROLL, VSCROLL"); out.println(" None or one or multiple axis value options can be specified."); out.println(" To specify multiple axes, use one axis option for per axis."); out.println(" Example: `scroll --axis VSCROLL,2 --axis SCROLL,-2.4`"); out.println(" keycombination [-t duration(ms)] <key code 1> <key code 2> ..." + " (Default: keyboard, the key order is important here.)"); } Loading Loading @@ -452,6 +517,62 @@ public class InputShellCommand extends ShellCommand { Float.parseFloat(getNextArgRequired()), displayId); } private void runScroll(int inputSource, int displayId) { inputSource = getSource(inputSource, InputDevice.SOURCE_ROTARY_ENCODER); final boolean isPointerEvent = (inputSource & SOURCE_CLASS_POINTER) == SOURCE_CLASS_POINTER; final Map<Integer, Float> axisValues = new HashMap<>(); if (isPointerEvent) { axisValues.put(AXIS_X, Float.parseFloat(getNextArgRequired())); axisValues.put(AXIS_Y, Float.parseFloat(getNextArgRequired())); } final Set<Integer> supportedAxes = Set.of(AXIS_HSCROLL, AXIS_VSCROLL, AXIS_SCROLL); String nextOption; while ((nextOption = getNextOption()) != null) { switch (nextOption) { case "--axis": final Pair<Integer, Float> axisAndValue = readAxisOptionValues(supportedAxes); axisValues.put(axisAndValue.first, axisAndValue.second); break; default: throw new IllegalArgumentException("Unsupported option: " + nextOption); } } final long now = SystemClock.uptimeMillis(); injectMotionEvent(inputSource, MotionEvent.ACTION_SCROLL, now /* downTime */, now /* when */, axisValues, displayId); } /** * Reads an axis value for the `--axis` command option. * * <p>The value for an `--axis` should be a single string containing the axis name without the * `AXIS_` prefix, and comma, and a float value representing the value for the respective axis. * * <p>Example: `--axis SCROLL,2.4` represents "a value of 2.4 for AXIS_SCROLL" * * <p>This method should be called after the `--axis` option has already been read. * * @param supportedAxes the set of allowed axes to be read. If an axis option is read where the * axis is not present in this set, this method throws an {@link IllegalArgumentException}. * @return a Pair of the axis and its respective value. */ private Pair<Integer, Float> readAxisOptionValues(Set<Integer> supportedAxes) { final String optionValue = getNextArgRequired(); final String[] axisAndValue = optionValue.split(","); if (axisAndValue.length != 2) { throw new IllegalArgumentException("Invalid --axis option value: " + optionValue); } final String axisName = "AXIS_" + axisAndValue[0]; final int axis = MotionEvent.axisFromString(axisName); if (axis == -1) { throw new IllegalArgumentException("Invalid axis name: " + axisName); } if (!supportedAxes.contains(axis)) { throw new IllegalArgumentException("Unsupported axis: " + axisName); } return Pair.create(axis, Float.parseFloat(axisAndValue[1])); } /** * Sends a simple zero-pressure move event. * Loading tests/Input/src/com/android/server/input/InputShellCommandTest.java 0 → 100644 +167 −0 Original line number Diff line number Diff line /* * Copyright 2023 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.InputDevice.SOURCE_MOUSE; import static android.view.InputDevice.SOURCE_ROTARY_ENCODER; import static android.view.MotionEvent.ACTION_SCROLL; import static android.view.MotionEvent.AXIS_HSCROLL; import static android.view.MotionEvent.AXIS_SCROLL; import static android.view.MotionEvent.AXIS_VSCROLL; import static android.view.MotionEvent.AXIS_X; import static android.view.MotionEvent.AXIS_Y; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import android.os.Binder; import android.view.InputEvent; import android.view.MotionEvent; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.FileDescriptor; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; /** * Build/Install/Run: * atest InputShellCommandTest */ @RunWith(AndroidJUnit4.class) public class InputShellCommandTest { private TestInputEventInjector mInputEventInjector = new TestInputEventInjector(); private InputShellCommand mCommand; @Before public void setUp() throws Exception { mCommand = new InputShellCommand(mInputEventInjector); } @Test public void testScroll_withPointerSource_noAxisOption() { runCommand("mouse scroll 2 -3"); MotionEvent event = (MotionEvent) getSingleInjectedInputEvent(); assertSourceAndAction(event, SOURCE_MOUSE, ACTION_SCROLL); assertAxisValues(event, Map.of(AXIS_X, 2f, AXIS_Y, -3f)); } @Test public void testScroll_withPointerSource_withScrollAxisOptions() { runCommand("mouse scroll 1 -2 --axis HSCROLL,3 --axis VSCROLL,1.7 --axis SCROLL,-4"); MotionEvent event = (MotionEvent) getSingleInjectedInputEvent(); assertSourceAndAction(event, SOURCE_MOUSE, ACTION_SCROLL); assertAxisValues( event, Map.of( AXIS_X, 1f, AXIS_Y, -2f, AXIS_HSCROLL, 3f, AXIS_VSCROLL, 1.7f, AXIS_SCROLL, -4f)); } @Test public void testScroll_withNonPointerSource_noAxisOption() { runCommand("rotaryencoder scroll"); MotionEvent event = (MotionEvent) getSingleInjectedInputEvent(); assertSourceAndAction(event, SOURCE_ROTARY_ENCODER, ACTION_SCROLL); } @Test public void testScroll_withNonPointerSource_withScrollAxisOptions() { runCommand("rotaryencoder scroll --axis HSCROLL,3 --axis VSCROLL,1.7 --axis SCROLL,-4"); MotionEvent event = (MotionEvent) getSingleInjectedInputEvent(); assertSourceAndAction(event, SOURCE_ROTARY_ENCODER, ACTION_SCROLL); assertAxisValues(event, Map.of(AXIS_HSCROLL, 3f, AXIS_VSCROLL, 1.7f, AXIS_SCROLL, -4f)); } @Test public void testDefaultScrollSource() { runCommand("scroll --axis SCROLL,-4"); MotionEvent event = (MotionEvent) getSingleInjectedInputEvent(); assertSourceAndAction(event, SOURCE_ROTARY_ENCODER, ACTION_SCROLL); assertAxisValues(event, Map.of(AXIS_SCROLL, -4f)); } @Test public void testInvalidScrollCommands() { runCommand("scroll --sdaxis SCROLL,-4"); // invalid option runCommand("scroll --axis MYAXIS,-4"); // invalid axis runCommand("scroll --AXIS SCROLL,-4"); // invalid axis option key runCommand("scroll --axis SCROLL,-4abc"); // invalid axis value assertThat(mInputEventInjector.mInjectedEvents).isEmpty(); } private InputEvent getSingleInjectedInputEvent() { assertThat(mInputEventInjector.mInjectedEvents).hasSize(1); return mInputEventInjector.mInjectedEvents.get(0); } private void assertSourceAndAction(MotionEvent event, int source, int action) { assertThat(event.getSource()).isEqualTo(source); assertThat(event.getAction()).isEqualTo(action); } private void assertAxisValues(MotionEvent event, Map<Integer, Float> expectedValues) { for (var entry : expectedValues.entrySet()) { final int axis = entry.getKey(); final float expectedValue = entry.getValue(); final float axisValue = event.getAxisValue(axis); assertWithMessage( String.format( "Expected [%f], found [%f] for axis %s", expectedValue, axisValue, MotionEvent.axisToString(axis))) .that(axisValue).isEqualTo(expectedValue); } } private void runCommand(String cmd) { mCommand.exec( new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), cmd.split(" ") /* args */); } private static class TestInputEventInjector implements BiConsumer<InputEvent, Integer> { List<InputEvent> mInjectedEvents = new ArrayList<>(); @Override public void accept(InputEvent event, Integer injectMode) { mInjectedEvents.add(event); } } } Loading
services/core/java/com/android/server/input/InputShellCommand.java +129 −8 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.input; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.InputDevice.SOURCE_CLASS_POINTER; import static android.view.KeyEvent.KEYCODE_ALT_LEFT; import static android.view.KeyEvent.KEYCODE_ALT_RIGHT; import static android.view.KeyEvent.KEYCODE_CTRL_LEFT; Loading @@ -38,6 +39,11 @@ import static android.view.KeyEvent.META_META_RIGHT_ON; import static android.view.KeyEvent.META_SHIFT_LEFT_ON; import static android.view.KeyEvent.META_SHIFT_ON; import static android.view.KeyEvent.META_SHIFT_RIGHT_ON; import static android.view.MotionEvent.AXIS_HSCROLL; import static android.view.MotionEvent.AXIS_SCROLL; import static android.view.MotionEvent.AXIS_VSCROLL; import static android.view.MotionEvent.AXIS_X; import static android.view.MotionEvent.AXIS_Y; import static java.util.Collections.unmodifiableMap; Loading @@ -47,14 +53,21 @@ import android.os.ShellCommand; import android.os.SystemClock; import android.util.ArrayMap; import android.util.IntArray; import android.util.Pair; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; /** * Command that sends input events to the device. Loading Loading @@ -107,15 +120,31 @@ public class InputShellCommand extends ShellCommand { map.put("touchpad", InputDevice.SOURCE_TOUCHPAD); map.put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION); map.put("joystick", InputDevice.SOURCE_JOYSTICK); map.put("rotaryencoder", InputDevice.SOURCE_ROTARY_ENCODER); SOURCES = unmodifiableMap(map); } public InputShellCommand() { this(InputShellCommand::injectInputEvent); } @VisibleForTesting InputShellCommand(BiConsumer<InputEvent, Integer> inputEventInjector) { mInputEventInjector = inputEventInjector;; } private static void injectInputEvent(InputEvent event, Integer injectMode) { InputManagerGlobal.getInstance().injectInputEvent(event, injectMode); } private final BiConsumer<InputEvent, Integer> mInputEventInjector; private void injectKeyEvent(KeyEvent event, boolean async) { int injectMode = async ? InputManager.INJECT_INPUT_EVENT_MODE_ASYNC : InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; InputManagerGlobal.getInstance().injectInputEvent(event, injectMode); mInputEventInjector.accept(event, injectMode); } private int getInputDeviceId(int inputSource) { Loading Loading @@ -161,19 +190,41 @@ public class InputShellCommand extends ShellCommand { */ private void injectMotionEvent(int inputSource, int action, long downTime, long when, float x, float y, float pressure, int displayId) { final Map<Integer, Float> axisValues = Map.of( MotionEvent.AXIS_X, x, MotionEvent.AXIS_Y, y, MotionEvent.AXIS_PRESSURE, pressure); injectMotionEvent(inputSource, action, downTime, when, axisValues, displayId); } /** * 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 axisValues a map of an axis to the respective axis value * @param displayId the ID of the display associated to the event */ private void injectMotionEvent(int inputSource, int action, long downTime, long when, Map<Integer, Float> axisValues, int displayId) { final int pointerCount = 1; MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[pointerCount]; MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount]; for (int i = 0; i < pointerCount; i++) { pointerProperties[i] = new MotionEvent.PointerProperties(); pointerProperties[i].id = i; pointerProperties[i].toolType = getToolType(inputSource); } MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount]; for (int i = 0; i < pointerCount; i++) { pointerCoords[i] = new MotionEvent.PointerCoords(); pointerCoords[i].x = x; pointerCoords[i].y = y; pointerCoords[i].pressure = pressure; pointerCoords[i].size = DEFAULT_SIZE; for (var entry : axisValues.entrySet()) { pointerCoords[i].setAxisValue(entry.getKey(), entry.getValue()); } } if (displayId == INVALID_DISPLAY && (inputSource & InputDevice.SOURCE_CLASS_POINTER) != 0) { Loading @@ -183,7 +234,7 @@ public class InputShellCommand extends ShellCommand { pointerProperties, pointerCoords, DEFAULT_META_STATE, DEFAULT_BUTTON_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, getInputDeviceId(inputSource), DEFAULT_EDGE_FLAGS, inputSource, displayId, DEFAULT_FLAGS); InputManagerGlobal.getInstance().injectInputEvent(event, mInputEventInjector.accept(event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); } Loading Loading @@ -246,6 +297,8 @@ public class InputShellCommand extends ShellCommand { runPress(inputSource, displayId); } else if ("roll".equals(arg)) { runRoll(inputSource, displayId); } else if ("scroll".equals(arg)) { runScroll(inputSource, displayId); } else if ("motionevent".equals(arg)) { runMotionEvent(inputSource, displayId); } else if ("keycombination".equals(arg)) { Loading @@ -268,13 +321,18 @@ public class InputShellCommand extends ShellCommand { for (String src : SOURCES.keySet()) { out.println(" " + src); } out.println("[axis_value] represents an option specifying the value of a given axis "); out.println(" The syntax is as follows: --axis <axis_name>,<axis_value>"); out.println(" where <axis_name> is the name of the axis as defined in "); out.println(" MotionEvent without the AXIS_ prefix (e.g. SCROLL, X)"); out.println(" Sample [axis_values] entry: `--axis Y,3`, `--axis SCROLL,-2`"); 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(" text <string> (Default: keyboard)"); out.println(" keyevent [--longpress|--doubletap|--async" + "|--delay <duration between keycodes in ms>]" + " <key code number or name> ..." Loading @@ -287,6 +345,13 @@ public class InputShellCommand extends ShellCommand { out.println(" press (Default: trackball)"); out.println(" roll <dx> <dy> (Default: trackball)"); out.println(" motionevent <DOWN|UP|MOVE|CANCEL> <x> <y> (Default: touchscreen)"); out.println(" scroll (Default: rotaryencoder). Has the following syntax:"); out.println(" scroll <x> <y> [axis_value] (for pointer-based sources)"); out.println(" scroll [axis_value] (for non-pointer-based sources)"); out.println(" Axis options: SCROLL, HSCROLL, VSCROLL"); out.println(" None or one or multiple axis value options can be specified."); out.println(" To specify multiple axes, use one axis option for per axis."); out.println(" Example: `scroll --axis VSCROLL,2 --axis SCROLL,-2.4`"); out.println(" keycombination [-t duration(ms)] <key code 1> <key code 2> ..." + " (Default: keyboard, the key order is important here.)"); } Loading Loading @@ -452,6 +517,62 @@ public class InputShellCommand extends ShellCommand { Float.parseFloat(getNextArgRequired()), displayId); } private void runScroll(int inputSource, int displayId) { inputSource = getSource(inputSource, InputDevice.SOURCE_ROTARY_ENCODER); final boolean isPointerEvent = (inputSource & SOURCE_CLASS_POINTER) == SOURCE_CLASS_POINTER; final Map<Integer, Float> axisValues = new HashMap<>(); if (isPointerEvent) { axisValues.put(AXIS_X, Float.parseFloat(getNextArgRequired())); axisValues.put(AXIS_Y, Float.parseFloat(getNextArgRequired())); } final Set<Integer> supportedAxes = Set.of(AXIS_HSCROLL, AXIS_VSCROLL, AXIS_SCROLL); String nextOption; while ((nextOption = getNextOption()) != null) { switch (nextOption) { case "--axis": final Pair<Integer, Float> axisAndValue = readAxisOptionValues(supportedAxes); axisValues.put(axisAndValue.first, axisAndValue.second); break; default: throw new IllegalArgumentException("Unsupported option: " + nextOption); } } final long now = SystemClock.uptimeMillis(); injectMotionEvent(inputSource, MotionEvent.ACTION_SCROLL, now /* downTime */, now /* when */, axisValues, displayId); } /** * Reads an axis value for the `--axis` command option. * * <p>The value for an `--axis` should be a single string containing the axis name without the * `AXIS_` prefix, and comma, and a float value representing the value for the respective axis. * * <p>Example: `--axis SCROLL,2.4` represents "a value of 2.4 for AXIS_SCROLL" * * <p>This method should be called after the `--axis` option has already been read. * * @param supportedAxes the set of allowed axes to be read. If an axis option is read where the * axis is not present in this set, this method throws an {@link IllegalArgumentException}. * @return a Pair of the axis and its respective value. */ private Pair<Integer, Float> readAxisOptionValues(Set<Integer> supportedAxes) { final String optionValue = getNextArgRequired(); final String[] axisAndValue = optionValue.split(","); if (axisAndValue.length != 2) { throw new IllegalArgumentException("Invalid --axis option value: " + optionValue); } final String axisName = "AXIS_" + axisAndValue[0]; final int axis = MotionEvent.axisFromString(axisName); if (axis == -1) { throw new IllegalArgumentException("Invalid axis name: " + axisName); } if (!supportedAxes.contains(axis)) { throw new IllegalArgumentException("Unsupported axis: " + axisName); } return Pair.create(axis, Float.parseFloat(axisAndValue[1])); } /** * Sends a simple zero-pressure move event. * Loading
tests/Input/src/com/android/server/input/InputShellCommandTest.java 0 → 100644 +167 −0 Original line number Diff line number Diff line /* * Copyright 2023 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.InputDevice.SOURCE_MOUSE; import static android.view.InputDevice.SOURCE_ROTARY_ENCODER; import static android.view.MotionEvent.ACTION_SCROLL; import static android.view.MotionEvent.AXIS_HSCROLL; import static android.view.MotionEvent.AXIS_SCROLL; import static android.view.MotionEvent.AXIS_VSCROLL; import static android.view.MotionEvent.AXIS_X; import static android.view.MotionEvent.AXIS_Y; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import android.os.Binder; import android.view.InputEvent; import android.view.MotionEvent; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.FileDescriptor; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; /** * Build/Install/Run: * atest InputShellCommandTest */ @RunWith(AndroidJUnit4.class) public class InputShellCommandTest { private TestInputEventInjector mInputEventInjector = new TestInputEventInjector(); private InputShellCommand mCommand; @Before public void setUp() throws Exception { mCommand = new InputShellCommand(mInputEventInjector); } @Test public void testScroll_withPointerSource_noAxisOption() { runCommand("mouse scroll 2 -3"); MotionEvent event = (MotionEvent) getSingleInjectedInputEvent(); assertSourceAndAction(event, SOURCE_MOUSE, ACTION_SCROLL); assertAxisValues(event, Map.of(AXIS_X, 2f, AXIS_Y, -3f)); } @Test public void testScroll_withPointerSource_withScrollAxisOptions() { runCommand("mouse scroll 1 -2 --axis HSCROLL,3 --axis VSCROLL,1.7 --axis SCROLL,-4"); MotionEvent event = (MotionEvent) getSingleInjectedInputEvent(); assertSourceAndAction(event, SOURCE_MOUSE, ACTION_SCROLL); assertAxisValues( event, Map.of( AXIS_X, 1f, AXIS_Y, -2f, AXIS_HSCROLL, 3f, AXIS_VSCROLL, 1.7f, AXIS_SCROLL, -4f)); } @Test public void testScroll_withNonPointerSource_noAxisOption() { runCommand("rotaryencoder scroll"); MotionEvent event = (MotionEvent) getSingleInjectedInputEvent(); assertSourceAndAction(event, SOURCE_ROTARY_ENCODER, ACTION_SCROLL); } @Test public void testScroll_withNonPointerSource_withScrollAxisOptions() { runCommand("rotaryencoder scroll --axis HSCROLL,3 --axis VSCROLL,1.7 --axis SCROLL,-4"); MotionEvent event = (MotionEvent) getSingleInjectedInputEvent(); assertSourceAndAction(event, SOURCE_ROTARY_ENCODER, ACTION_SCROLL); assertAxisValues(event, Map.of(AXIS_HSCROLL, 3f, AXIS_VSCROLL, 1.7f, AXIS_SCROLL, -4f)); } @Test public void testDefaultScrollSource() { runCommand("scroll --axis SCROLL,-4"); MotionEvent event = (MotionEvent) getSingleInjectedInputEvent(); assertSourceAndAction(event, SOURCE_ROTARY_ENCODER, ACTION_SCROLL); assertAxisValues(event, Map.of(AXIS_SCROLL, -4f)); } @Test public void testInvalidScrollCommands() { runCommand("scroll --sdaxis SCROLL,-4"); // invalid option runCommand("scroll --axis MYAXIS,-4"); // invalid axis runCommand("scroll --AXIS SCROLL,-4"); // invalid axis option key runCommand("scroll --axis SCROLL,-4abc"); // invalid axis value assertThat(mInputEventInjector.mInjectedEvents).isEmpty(); } private InputEvent getSingleInjectedInputEvent() { assertThat(mInputEventInjector.mInjectedEvents).hasSize(1); return mInputEventInjector.mInjectedEvents.get(0); } private void assertSourceAndAction(MotionEvent event, int source, int action) { assertThat(event.getSource()).isEqualTo(source); assertThat(event.getAction()).isEqualTo(action); } private void assertAxisValues(MotionEvent event, Map<Integer, Float> expectedValues) { for (var entry : expectedValues.entrySet()) { final int axis = entry.getKey(); final float expectedValue = entry.getValue(); final float axisValue = event.getAxisValue(axis); assertWithMessage( String.format( "Expected [%f], found [%f] for axis %s", expectedValue, axisValue, MotionEvent.axisToString(axis))) .that(axisValue).isEqualTo(expectedValue); } } private void runCommand(String cmd) { mCommand.exec( new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), cmd.split(" ") /* args */); } private static class TestInputEventInjector implements BiConsumer<InputEvent, Integer> { List<InputEvent> mInjectedEvents = new ArrayList<>(); @Override public void accept(InputEvent event, Integer injectMode) { mInjectedEvents.add(event); } } }