Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3e920dd1 authored by Yeabkal Wubshit's avatar Yeabkal Wubshit Committed by Android (Google) Code Review
Browse files

Merge "Add support for adb scroll event injection" into main

parents af4120d0 724215e2
Loading
Loading
Loading
Loading
+129 −8
Original line number Diff line number Diff line
@@ -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;
@@ -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;

@@ -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.
@@ -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) {
@@ -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) {
@@ -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);
    }

@@ -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)) {
@@ -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> ..."
@@ -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.)");
        }
@@ -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.
     *
+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);
        }
    }
}