From c4cf03b162a3ce0fddee7d913aca45ff178b5242 Mon Sep 17 00:00:00 2001 From: Christine Franks Date: Fri, 19 Nov 2021 14:46:16 -0800 Subject: Remote input interfaces, events, and injection Bug: 204081582 CTS-Coverage-Bug: 208247880 Test: atest FrameworksServicesTests:VirtualDeviceManagerServiceTest and atest FrameworksCoreTests:android.hardware.input Change-Id: Ic10e62b0d95d563aed59ed9bb8caba6d506176b7 --- core/api/system-current.txt | 121 ++++++ .../android/companion/virtual/IVirtualDevice.aidl | 32 ++ .../companion/virtual/VirtualDeviceManager.java | 91 +++++ .../android/hardware/input/VirtualKeyEvent.aidl | 19 + .../android/hardware/input/VirtualKeyEvent.java | 152 +++++++ .../android/hardware/input/VirtualKeyboard.java | 72 ++++ core/java/android/hardware/input/VirtualMouse.java | 102 +++++ .../hardware/input/VirtualMouseButtonEvent.aidl | 19 + .../hardware/input/VirtualMouseButtonEvent.java | 180 ++++++++ .../hardware/input/VirtualMouseRelativeEvent.aidl | 19 + .../hardware/input/VirtualMouseRelativeEvent.java | 119 ++++++ .../hardware/input/VirtualMouseScrollEvent.aidl | 19 + .../hardware/input/VirtualMouseScrollEvent.java | 129 ++++++ .../android/hardware/input/VirtualTouchEvent.aidl | 19 + .../android/hardware/input/VirtualTouchEvent.java | 299 ++++++++++++++ .../android/hardware/input/VirtualTouchscreen.java | 71 ++++ core/java/android/view/MotionEvent.java | 9 + .../hardware/input/VirtualKeyEventTest.java | 60 +++ .../input/VirtualMouseButtonEventTest.java | 59 +++ .../input/VirtualMouseRelativeEventTest.java | 37 ++ .../input/VirtualMouseScrollEventTest.java | 54 +++ .../hardware/input/VirtualTouchEventTest.java | 172 ++++++++ .../server/companion/virtual/InputController.java | 283 +++++++++++++ .../companion/virtual/VirtualDeviceImpl.java | 278 +++++++++++++ .../virtual/VirtualDeviceManagerService.java | 80 +--- services/core/jni/Android.bp | 1 + ...id_server_companion_virtual_InputController.cpp | 455 +++++++++++++++++++++ services/core/jni/onload.cpp | 2 + services/tests/servicestests/Android.bp | 1 + .../virtual/VirtualDeviceManagerServiceTest.java | 313 ++++++++++++++ 30 files changed, 3203 insertions(+), 64 deletions(-) create mode 100644 core/java/android/hardware/input/VirtualKeyEvent.aidl create mode 100644 core/java/android/hardware/input/VirtualKeyEvent.java create mode 100644 core/java/android/hardware/input/VirtualKeyboard.java create mode 100644 core/java/android/hardware/input/VirtualMouse.java create mode 100644 core/java/android/hardware/input/VirtualMouseButtonEvent.aidl create mode 100644 core/java/android/hardware/input/VirtualMouseButtonEvent.java create mode 100644 core/java/android/hardware/input/VirtualMouseRelativeEvent.aidl create mode 100644 core/java/android/hardware/input/VirtualMouseRelativeEvent.java create mode 100644 core/java/android/hardware/input/VirtualMouseScrollEvent.aidl create mode 100644 core/java/android/hardware/input/VirtualMouseScrollEvent.java create mode 100644 core/java/android/hardware/input/VirtualTouchEvent.aidl create mode 100644 core/java/android/hardware/input/VirtualTouchEvent.java create mode 100644 core/java/android/hardware/input/VirtualTouchscreen.java create mode 100644 core/tests/coretests/src/android/hardware/input/VirtualKeyEventTest.java create mode 100644 core/tests/coretests/src/android/hardware/input/VirtualMouseButtonEventTest.java create mode 100644 core/tests/coretests/src/android/hardware/input/VirtualMouseRelativeEventTest.java create mode 100644 core/tests/coretests/src/android/hardware/input/VirtualMouseScrollEventTest.java create mode 100644 core/tests/coretests/src/android/hardware/input/VirtualTouchEventTest.java create mode 100644 services/companion/java/com/android/server/companion/virtual/InputController.java create mode 100644 services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java create mode 100644 services/core/jni/com_android_server_companion_virtual_InputController.cpp create mode 100644 services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java diff --git a/core/api/system-current.txt b/core/api/system-current.txt index c1d151813092..576a2b3f2e95 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3889,6 +3889,127 @@ package android.hardware.hdmi { } +package android.hardware.input { + + public final class VirtualKeyEvent implements android.os.Parcelable { + method public int describeContents(); + method public int getAction(); + method public int getKeyCode(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int ACTION_DOWN = 0; // 0x0 + field public static final int ACTION_UP = 1; // 0x1 + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class VirtualKeyEvent.Builder { + ctor public VirtualKeyEvent.Builder(); + method @NonNull public android.hardware.input.VirtualKeyEvent build(); + method @NonNull public android.hardware.input.VirtualKeyEvent.Builder setAction(int); + method @NonNull public android.hardware.input.VirtualKeyEvent.Builder setKeyCode(int); + } + + public class VirtualKeyboard implements java.io.Closeable { + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close(); + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent); + } + + public class VirtualMouse implements java.io.Closeable { + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close(); + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendButtonEvent(@NonNull android.hardware.input.VirtualMouseButtonEvent); + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendRelativeEvent(@NonNull android.hardware.input.VirtualMouseRelativeEvent); + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendScrollEvent(@NonNull android.hardware.input.VirtualMouseScrollEvent); + } + + public final class VirtualMouseButtonEvent implements android.os.Parcelable { + method public int describeContents(); + method public int getAction(); + method public int getButtonCode(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int ACTION_BUTTON_PRESS = 11; // 0xb + field public static final int ACTION_BUTTON_RELEASE = 12; // 0xc + field public static final int BUTTON_BACK = 8; // 0x8 + field public static final int BUTTON_FORWARD = 16; // 0x10 + field public static final int BUTTON_PRIMARY = 1; // 0x1 + field public static final int BUTTON_SECONDARY = 2; // 0x2 + field public static final int BUTTON_TERTIARY = 4; // 0x4 + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class VirtualMouseButtonEvent.Builder { + ctor public VirtualMouseButtonEvent.Builder(); + method @NonNull public android.hardware.input.VirtualMouseButtonEvent build(); + method @NonNull public android.hardware.input.VirtualMouseButtonEvent.Builder setAction(int); + method @NonNull public android.hardware.input.VirtualMouseButtonEvent.Builder setButtonCode(int); + } + + public final class VirtualMouseRelativeEvent implements android.os.Parcelable { + method public int describeContents(); + method public float getRelativeX(); + method public float getRelativeY(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class VirtualMouseRelativeEvent.Builder { + ctor public VirtualMouseRelativeEvent.Builder(); + method @NonNull public android.hardware.input.VirtualMouseRelativeEvent build(); + method @NonNull public android.hardware.input.VirtualMouseRelativeEvent.Builder setRelativeX(float); + method @NonNull public android.hardware.input.VirtualMouseRelativeEvent.Builder setRelativeY(float); + } + + public final class VirtualMouseScrollEvent implements android.os.Parcelable { + method public int describeContents(); + method public float getXAxisMovement(); + method public float getYAxisMovement(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class VirtualMouseScrollEvent.Builder { + ctor public VirtualMouseScrollEvent.Builder(); + method @NonNull public android.hardware.input.VirtualMouseScrollEvent build(); + method @NonNull public android.hardware.input.VirtualMouseScrollEvent.Builder setXAxisMovement(@FloatRange(from=-1.0F, to=1.0f) float); + method @NonNull public android.hardware.input.VirtualMouseScrollEvent.Builder setYAxisMovement(@FloatRange(from=-1.0F, to=1.0f) float); + } + + public final class VirtualTouchEvent implements android.os.Parcelable { + method public int describeContents(); + method public int getAction(); + method public float getMajorAxisSize(); + method public int getPointerId(); + method public float getPressure(); + method public int getToolType(); + method public float getX(); + method public float getY(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int ACTION_CANCEL = 3; // 0x3 + field public static final int ACTION_DOWN = 0; // 0x0 + field public static final int ACTION_MOVE = 2; // 0x2 + field public static final int ACTION_UP = 1; // 0x1 + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + field public static final int TOOL_TYPE_FINGER = 1; // 0x1 + field public static final int TOOL_TYPE_PALM = 5; // 0x5 + } + + public static final class VirtualTouchEvent.Builder { + ctor public VirtualTouchEvent.Builder(); + method @NonNull public android.hardware.input.VirtualTouchEvent build(); + method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setAction(int); + method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setMajorAxisSize(@FloatRange(from=0.0f) float); + method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setPointerId(int); + method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setPressure(@FloatRange(from=0.0f) float); + method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setToolType(int); + method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setX(float); + method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setY(float); + } + + public class VirtualTouchscreen implements java.io.Closeable { + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close(); + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent); + } + +} + package android.hardware.lights { public final class LightState implements android.os.Parcelable { diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl index dabc603bc47f..82ad15057fe3 100644 --- a/core/java/android/companion/virtual/IVirtualDevice.aidl +++ b/core/java/android/companion/virtual/IVirtualDevice.aidl @@ -16,6 +16,13 @@ package android.companion.virtual; +import android.graphics.Point; +import android.hardware.input.VirtualKeyEvent; +import android.hardware.input.VirtualMouseButtonEvent; +import android.hardware.input.VirtualMouseRelativeEvent; +import android.hardware.input.VirtualMouseScrollEvent; +import android.hardware.input.VirtualTouchEvent; + /** * Interface for a virtual device. * @@ -34,4 +41,29 @@ interface IVirtualDevice { * Closes the virtual device and frees all associated resources. */ void close(); + void createVirtualKeyboard( + int displayId, + String inputDeviceName, + int vendorId, + int productId, + IBinder token); + void createVirtualMouse( + int displayId, + String inputDeviceName, + int vendorId, + int productId, + IBinder token); + void createVirtualTouchscreen( + int displayId, + String inputDeviceName, + int vendorId, + int productId, + IBinder token, + in Point screenSize); + void unregisterInputDevice(IBinder token); + boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event); + boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event); + boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event); + boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event); + boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event); } diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 590b10887c7f..0d024b1d3200 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -23,7 +23,13 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.companion.AssociationInfo; import android.content.Context; +import android.graphics.Point; +import android.hardware.display.VirtualDisplay; +import android.hardware.input.VirtualKeyboard; +import android.hardware.input.VirtualMouse; +import android.hardware.input.VirtualTouchscreen; import android.os.Binder; +import android.os.IBinder; import android.os.RemoteException; /** @@ -73,6 +79,8 @@ public final class VirtualDeviceManager { * A virtual device has its own virtual display, audio output, microphone, and camera etc. The * creator of a virtual device can take the output from the virtual display and stream it over * to another device, and inject input events that are received from the remote device. + * + * TODO(b/204081582): Consider using a builder pattern for the input APIs. */ public static class VirtualDevice implements AutoCloseable { @@ -95,5 +103,88 @@ public final class VirtualDeviceManager { throw e.rethrowFromSystemServer(); } } + + /** + * Creates a virtual keyboard. + * + * @param display the display that the events inputted through this device should target + * @param inputDeviceName the name to call this input device + * @param vendorId the vendor id + * @param productId the product id + * @hide + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + @NonNull + public VirtualKeyboard createVirtualKeyboard( + @NonNull VirtualDisplay display, + @NonNull String inputDeviceName, + int vendorId, + int productId) { + try { + final IBinder token = new Binder( + "android.hardware.input.VirtualKeyboard:" + inputDeviceName); + mVirtualDevice.createVirtualKeyboard(display.getDisplay().getDisplayId(), + inputDeviceName, vendorId, productId, token); + return new VirtualKeyboard(mVirtualDevice, token); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Creates a virtual mouse. + * + * @param display the display that the events inputted through this device should target + * @param inputDeviceName the name to call this input device + * @param vendorId the vendor id + * @param productId the product id + * @hide + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + @NonNull + public VirtualMouse createVirtualMouse( + @NonNull VirtualDisplay display, + @NonNull String inputDeviceName, + int vendorId, + int productId) { + try { + final IBinder token = new Binder( + "android.hardware.input.VirtualMouse:" + inputDeviceName); + mVirtualDevice.createVirtualMouse(display.getDisplay().getDisplayId(), + inputDeviceName, vendorId, productId, token); + return new VirtualMouse(mVirtualDevice, token); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Creates a virtual touchscreen. + * + * @param display the display that the events inputted through this device should target + * @param inputDeviceName the name to call this input device + * @param vendorId the vendor id + * @param productId the product id + * @hide + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + @NonNull + public VirtualTouchscreen createVirtualTouchscreen( + @NonNull VirtualDisplay display, + @NonNull String inputDeviceName, + int vendorId, + int productId) { + try { + final IBinder token = new Binder( + "android.hardware.input.VirtualTouchscreen:" + inputDeviceName); + final Point size = new Point(); + display.getDisplay().getSize(size); + mVirtualDevice.createVirtualTouchscreen(display.getDisplay().getDisplayId(), + inputDeviceName, vendorId, productId, token, size); + return new VirtualTouchscreen(mVirtualDevice, token); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } } diff --git a/core/java/android/hardware/input/VirtualKeyEvent.aidl b/core/java/android/hardware/input/VirtualKeyEvent.aidl new file mode 100644 index 000000000000..5b3ee0c985bd --- /dev/null +++ b/core/java/android/hardware/input/VirtualKeyEvent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +parcelable VirtualKeyEvent; \ No newline at end of file diff --git a/core/java/android/hardware/input/VirtualKeyEvent.java b/core/java/android/hardware/input/VirtualKeyEvent.java new file mode 100644 index 000000000000..d875156f5dc7 --- /dev/null +++ b/core/java/android/hardware/input/VirtualKeyEvent.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.KeyEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * An event describing a keyboard interaction originating from a remote device. + * + * When the user presses a key, an {@code ACTION_DOWN} event should be reported. When the user + * releases the key, an {@code ACTION_UP} event should be reported. + * + * See {@link android.view.KeyEvent}. + * + * @hide + */ +@SystemApi +public final class VirtualKeyEvent implements Parcelable { + + /** @hide */ + public static final int ACTION_UNKNOWN = -1; + /** Action indicating the given key has been pressed. */ + public static final int ACTION_DOWN = KeyEvent.ACTION_DOWN; + /** Action indicating the previously pressed key has been lifted. */ + public static final int ACTION_UP = KeyEvent.ACTION_UP; + + /** @hide */ + @IntDef(prefix = { "ACTION_" }, value = { + ACTION_UNKNOWN, + ACTION_DOWN, + ACTION_UP, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Action { + } + + private final @Action int mAction; + private final int mKeyCode; + + private VirtualKeyEvent(@Action int action, int keyCode) { + mAction = action; + mKeyCode = keyCode; + } + + private VirtualKeyEvent(@NonNull Parcel parcel) { + mAction = parcel.readInt(); + mKeyCode = parcel.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) { + parcel.writeInt(mAction); + parcel.writeInt(mKeyCode); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Returns the key code associated with this event. + */ + public int getKeyCode() { + return mKeyCode; + } + + /** + * Returns the action associated with this event. + */ + public @Action int getAction() { + return mAction; + } + + /** + * Builder for {@link VirtualKeyEvent}. + */ + public static final class Builder { + + private @Action int mAction = ACTION_UNKNOWN; + private int mKeyCode = -1; + + /** + * Creates a {@link VirtualKeyEvent} object with the current builder configuration. + */ + public @NonNull VirtualKeyEvent build() { + if (mAction == ACTION_UNKNOWN || mKeyCode == -1) { + throw new IllegalArgumentException( + "Cannot build virtual key event with unset fields"); + } + return new VirtualKeyEvent(mAction, mKeyCode); + } + + /** + * Sets the Android key code of the event. The set of allowed characters include digits 0-9, + * characters A-Z, and standard punctuation, as well as numpad keys, function keys F1-F12, + * and meta keys (caps lock, shift, etc.). + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setKeyCode(int keyCode) { + mKeyCode = keyCode; + return this; + } + + /** + * Sets the action of the event. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setAction(@Action int action) { + if (action != ACTION_DOWN && action != ACTION_UP) { + throw new IllegalArgumentException("Unsupported action type"); + } + mAction = action; + return this; + } + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public VirtualKeyEvent createFromParcel(Parcel source) { + return new VirtualKeyEvent(source); + } + + public VirtualKeyEvent[] newArray(int size) { + return new VirtualKeyEvent[size]; + } + }; +} diff --git a/core/java/android/hardware/input/VirtualKeyboard.java b/core/java/android/hardware/input/VirtualKeyboard.java new file mode 100644 index 000000000000..ee9b659e9521 --- /dev/null +++ b/core/java/android/hardware/input/VirtualKeyboard.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.companion.virtual.IVirtualDevice; +import android.os.IBinder; +import android.os.RemoteException; + +import java.io.Closeable; + +/** + * A virtual keyboard representing a key input mechanism on a remote device, such as a built-in + * keyboard on a laptop, a software keyboard on a tablet, or a keypad on a TV remote control. + * + * This registers an InputDevice that is interpreted like a physically-connected device and + * dispatches received events to it. + * + * @hide + */ +@SystemApi +public class VirtualKeyboard implements Closeable { + + private final IVirtualDevice mVirtualDevice; + private final IBinder mToken; + + /** @hide */ + public VirtualKeyboard(IVirtualDevice virtualDevice, IBinder token) { + mVirtualDevice = virtualDevice; + mToken = token; + } + + @Override + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void close() { + try { + mVirtualDevice.unregisterInputDevice(mToken); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Sends a key event to the system. + * + * @param event the event to send + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void sendKeyEvent(@NonNull VirtualKeyEvent event) { + try { + mVirtualDevice.sendKeyEvent(mToken, event); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/hardware/input/VirtualMouse.java b/core/java/android/hardware/input/VirtualMouse.java new file mode 100644 index 000000000000..6599dd2e28eb --- /dev/null +++ b/core/java/android/hardware/input/VirtualMouse.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.companion.virtual.IVirtualDevice; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.MotionEvent; + +import java.io.Closeable; + +/** + * A virtual mouse representing a relative input mechanism on a remote device, such as a mouse or + * trackpad. + * + * This registers an InputDevice that is interpreted like a physically-connected device and + * dispatches received events to it. + * + * @hide + */ +@SystemApi +public class VirtualMouse implements Closeable { + + private final IVirtualDevice mVirtualDevice; + private final IBinder mToken; + + /** @hide */ + public VirtualMouse(IVirtualDevice virtualDevice, IBinder token) { + mVirtualDevice = virtualDevice; + mToken = token; + } + + @Override + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void close() { + try { + mVirtualDevice.unregisterInputDevice(mToken); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Send a mouse button event to the system. + * + * @param event the event + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void sendButtonEvent(@NonNull VirtualMouseButtonEvent event) { + try { + mVirtualDevice.sendButtonEvent(mToken, event); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Sends a scrolling event to the system. See {@link MotionEvent#AXIS_VSCROLL} and + * {@link MotionEvent#AXIS_SCROLL}. + * + * @param event the event + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void sendScrollEvent(@NonNull VirtualMouseScrollEvent event) { + try { + mVirtualDevice.sendScrollEvent(mToken, event); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Sends a relative movement event to the system. + * + * @param event the event + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void sendRelativeEvent(@NonNull VirtualMouseRelativeEvent event) { + try { + mVirtualDevice.sendRelativeEvent(mToken, event); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/hardware/input/VirtualMouseButtonEvent.aidl b/core/java/android/hardware/input/VirtualMouseButtonEvent.aidl new file mode 100644 index 000000000000..ebcf5aad4066 --- /dev/null +++ b/core/java/android/hardware/input/VirtualMouseButtonEvent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +parcelable VirtualMouseButtonEvent; \ No newline at end of file diff --git a/core/java/android/hardware/input/VirtualMouseButtonEvent.java b/core/java/android/hardware/input/VirtualMouseButtonEvent.java new file mode 100644 index 000000000000..2e094cfb4e24 --- /dev/null +++ b/core/java/android/hardware/input/VirtualMouseButtonEvent.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.MotionEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * An event describing a mouse button click interaction originating from a remote device. + * + * @hide + */ +@SystemApi +public final class VirtualMouseButtonEvent implements Parcelable { + + /** @hide */ + public static final int ACTION_UNKNOWN = -1; + /** Action indicating the mouse button has been pressed. */ + public static final int ACTION_BUTTON_PRESS = MotionEvent.ACTION_BUTTON_PRESS; + /** Action indicating the mouse button has been released. */ + public static final int ACTION_BUTTON_RELEASE = MotionEvent.ACTION_BUTTON_RELEASE; + /** @hide */ + @IntDef(prefix = {"ACTION_"}, value = { + ACTION_UNKNOWN, + ACTION_BUTTON_PRESS, + ACTION_BUTTON_RELEASE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Action {} + + /** @hide */ + public static final int BUTTON_UNKNOWN = -1; + /** Action indicating the mouse button involved in this event is in the left position. */ + public static final int BUTTON_PRIMARY = MotionEvent.BUTTON_PRIMARY; + /** Action indicating the mouse button involved in this event is in the middle position. */ + public static final int BUTTON_TERTIARY = MotionEvent.BUTTON_TERTIARY; + /** Action indicating the mouse button involved in this event is in the right position. */ + public static final int BUTTON_SECONDARY = MotionEvent.BUTTON_SECONDARY; + /** + * Action indicating the mouse button involved in this event is intended to go back to the + * previous. + */ + public static final int BUTTON_BACK = MotionEvent.BUTTON_BACK; + /** + * Action indicating the mouse button involved in this event is intended to move forward to the + * next. + */ + public static final int BUTTON_FORWARD = MotionEvent.BUTTON_FORWARD; + /** @hide */ + @IntDef(prefix = {"BUTTON_"}, value = { + BUTTON_UNKNOWN, + BUTTON_PRIMARY, + BUTTON_TERTIARY, + BUTTON_SECONDARY, + BUTTON_BACK, + BUTTON_FORWARD, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Button {} + + private final @Action int mAction; + private final @Button int mButtonCode; + + private VirtualMouseButtonEvent(@Action int action, @Button int buttonCode) { + mAction = action; + mButtonCode = buttonCode; + } + + private VirtualMouseButtonEvent(@NonNull Parcel parcel) { + mAction = parcel.readInt(); + mButtonCode = parcel.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) { + parcel.writeInt(mAction); + parcel.writeInt(mButtonCode); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Returns the button code associated with this event. + */ + public @Button int getButtonCode() { + return mButtonCode; + } + + /** + * Returns the action associated with this event. + */ + public @Action int getAction() { + return mAction; + } + + /** + * Builder for {@link VirtualMouseButtonEvent}. + */ + public static final class Builder { + + private @Action int mAction = ACTION_UNKNOWN; + private @Button int mButtonCode = -1; + + /** + * Creates a {@link VirtualMouseButtonEvent} object with the current builder configuration. + */ + public @NonNull VirtualMouseButtonEvent build() { + if (mAction == ACTION_UNKNOWN || mButtonCode == -1) { + throw new IllegalArgumentException( + "Cannot build virtual mouse button event with unset fields"); + } + return new VirtualMouseButtonEvent(mAction, mButtonCode); + } + + /** + * Sets the button code of the event. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setButtonCode(int buttonCode) { + if (buttonCode != BUTTON_PRIMARY + && buttonCode != BUTTON_TERTIARY + && buttonCode != BUTTON_SECONDARY + && buttonCode != BUTTON_BACK + && buttonCode != BUTTON_FORWARD) { + throw new IllegalArgumentException("Unsupported mouse button code"); + } + mButtonCode = buttonCode; + return this; + } + + /** + * Sets the action of the event. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setAction(@Action int action) { + if (action != ACTION_BUTTON_PRESS && action != ACTION_BUTTON_RELEASE) { + throw new IllegalArgumentException("Unsupported mouse button action type"); + } + mAction = action; + return this; + } + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public VirtualMouseButtonEvent createFromParcel(Parcel source) { + return new VirtualMouseButtonEvent(source); + } + + public VirtualMouseButtonEvent[] newArray(int size) { + return new VirtualMouseButtonEvent[size]; + } + }; +} diff --git a/core/java/android/hardware/input/VirtualMouseRelativeEvent.aidl b/core/java/android/hardware/input/VirtualMouseRelativeEvent.aidl new file mode 100644 index 000000000000..1095858fde21 --- /dev/null +++ b/core/java/android/hardware/input/VirtualMouseRelativeEvent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +parcelable VirtualMouseRelativeEvent; \ No newline at end of file diff --git a/core/java/android/hardware/input/VirtualMouseRelativeEvent.java b/core/java/android/hardware/input/VirtualMouseRelativeEvent.java new file mode 100644 index 000000000000..65ed1f2f6f3a --- /dev/null +++ b/core/java/android/hardware/input/VirtualMouseRelativeEvent.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * An event describing a mouse movement interaction originating from a remote device. + * + * See {@link android.view.MotionEvent}. + * + * @hide + */ +@SystemApi +public final class VirtualMouseRelativeEvent implements Parcelable { + + private final float mRelativeX; + private final float mRelativeY; + + private VirtualMouseRelativeEvent(float relativeX, float relativeY) { + mRelativeX = relativeX; + mRelativeY = relativeY; + } + + private VirtualMouseRelativeEvent(@NonNull Parcel parcel) { + mRelativeX = parcel.readFloat(); + mRelativeY = parcel.readFloat(); + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) { + parcel.writeFloat(mRelativeX); + parcel.writeFloat(mRelativeY); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Returns the relative x-axis movement, in pixels. + */ + public float getRelativeX() { + return mRelativeX; + } + + /** + * Returns the relative x-axis movement, in pixels. + */ + public float getRelativeY() { + return mRelativeY; + } + + /** + * Builder for {@link VirtualMouseRelativeEvent}. + */ + public static final class Builder { + + private float mRelativeX; + private float mRelativeY; + + /** + * Creates a {@link VirtualMouseRelativeEvent} object with the current builder + * configuration. + */ + public @NonNull VirtualMouseRelativeEvent build() { + return new VirtualMouseRelativeEvent(mRelativeX, mRelativeY); + } + + /** + * Sets the relative x-axis movement, in pixels. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setRelativeX(float relativeX) { + mRelativeX = relativeX; + return this; + } + + /** + * Sets the relative y-axis movement, in pixels. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setRelativeY(float relativeY) { + mRelativeY = relativeY; + return this; + } + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public VirtualMouseRelativeEvent createFromParcel(Parcel source) { + return new VirtualMouseRelativeEvent(source); + } + + public VirtualMouseRelativeEvent[] newArray(int size) { + return new VirtualMouseRelativeEvent[size]; + } + }; +} diff --git a/core/java/android/hardware/input/VirtualMouseScrollEvent.aidl b/core/java/android/hardware/input/VirtualMouseScrollEvent.aidl new file mode 100644 index 000000000000..13177efcbb62 --- /dev/null +++ b/core/java/android/hardware/input/VirtualMouseScrollEvent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +parcelable VirtualMouseScrollEvent; \ No newline at end of file diff --git a/core/java/android/hardware/input/VirtualMouseScrollEvent.java b/core/java/android/hardware/input/VirtualMouseScrollEvent.java new file mode 100644 index 000000000000..1723259ba4b7 --- /dev/null +++ b/core/java/android/hardware/input/VirtualMouseScrollEvent.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import android.annotation.FloatRange; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +/** + * An event describing a mouse scroll interaction originating from a remote device. + * + * See {@link android.view.MotionEvent}. + * + * @hide + */ +@SystemApi +public final class VirtualMouseScrollEvent implements Parcelable { + + private final float mXAxisMovement; + private final float mYAxisMovement; + + private VirtualMouseScrollEvent(float xAxisMovement, float yAxisMovement) { + mXAxisMovement = xAxisMovement; + mYAxisMovement = yAxisMovement; + } + + private VirtualMouseScrollEvent(@NonNull Parcel parcel) { + mXAxisMovement = parcel.readFloat(); + mYAxisMovement = parcel.readFloat(); + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) { + parcel.writeFloat(mXAxisMovement); + parcel.writeFloat(mYAxisMovement); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Returns the x-axis scroll movement, normalized from -1.0 to 1.0, inclusive. Positive values + * indicate scrolling upward; negative values, downward. + */ + public float getXAxisMovement() { + return mXAxisMovement; + } + + /** + * Returns the y-axis scroll movement, normalized from -1.0 to 1.0, inclusive. Positive values + * indicate scrolling towards the right; negative values, to the left. + */ + public float getYAxisMovement() { + return mYAxisMovement; + } + + /** + * Builder for {@link VirtualMouseScrollEvent}. + */ + public static final class Builder { + + private float mXAxisMovement; + private float mYAxisMovement; + + /** + * Creates a {@link VirtualMouseScrollEvent} object with the current builder configuration. + */ + public @NonNull VirtualMouseScrollEvent build() { + return new VirtualMouseScrollEvent(mXAxisMovement, mYAxisMovement); + } + + /** + * Sets the x-axis scroll movement, normalized from -1.0 to 1.0, inclusive. Positive values + * indicate scrolling upward; negative values, downward. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setXAxisMovement( + @FloatRange(from = -1.0f, to = 1.0f) float xAxisMovement) { + Preconditions.checkArgumentInRange(xAxisMovement, -1f, 1f, "xAxisMovement"); + mXAxisMovement = xAxisMovement; + return this; + } + + /** + * Sets the y-axis scroll movement, normalized from -1.0 to 1.0, inclusive. Positive values + * indicate scrolling towards the right; negative values, to the left. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setYAxisMovement( + @FloatRange(from = -1.0f, to = 1.0f) float yAxisMovement) { + Preconditions.checkArgumentInRange(yAxisMovement, -1f, 1f, "yAxisMovement"); + mYAxisMovement = yAxisMovement; + return this; + } + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public VirtualMouseScrollEvent createFromParcel(Parcel source) { + return new VirtualMouseScrollEvent(source); + } + + public VirtualMouseScrollEvent[] newArray(int size) { + return new VirtualMouseScrollEvent[size]; + } + }; +} diff --git a/core/java/android/hardware/input/VirtualTouchEvent.aidl b/core/java/android/hardware/input/VirtualTouchEvent.aidl new file mode 100644 index 000000000000..03c82e3eef3a --- /dev/null +++ b/core/java/android/hardware/input/VirtualTouchEvent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +parcelable VirtualTouchEvent; \ No newline at end of file diff --git a/core/java/android/hardware/input/VirtualTouchEvent.java b/core/java/android/hardware/input/VirtualTouchEvent.java new file mode 100644 index 000000000000..c7450d8fa65d --- /dev/null +++ b/core/java/android/hardware/input/VirtualTouchEvent.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import android.annotation.FloatRange; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.MotionEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * An event describing a touchscreen interaction originating from a remote device. + * + * The pointer id, tool type, action, and location are required; pressure and main axis size are + * optional. + * + * @hide + */ +@SystemApi +public final class VirtualTouchEvent implements Parcelable { + + /** @hide */ + public static final int TOOL_TYPE_UNKNOWN = MotionEvent.TOOL_TYPE_UNKNOWN; + /** Tool type indicating that the user's finger is the origin of the event. */ + public static final int TOOL_TYPE_FINGER = MotionEvent.TOOL_TYPE_FINGER; + /** + * Tool type indicating that a user's palm (or other input mechanism to be rejected) is the + * origin of the event. + */ + public static final int TOOL_TYPE_PALM = MotionEvent.TOOL_TYPE_PALM; + /** @hide */ + @IntDef(prefix = { "TOOL_TYPE_" }, value = { + TOOL_TYPE_UNKNOWN, + TOOL_TYPE_FINGER, + TOOL_TYPE_PALM, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ToolType {} + + /** @hide */ + public static final int ACTION_UNKNOWN = -1; + /** Action indicating the tool has been pressed down to the touchscreen. */ + public static final int ACTION_DOWN = MotionEvent.ACTION_DOWN; + /** Action indicating the tool has been lifted from the touchscreen. */ + public static final int ACTION_UP = MotionEvent.ACTION_UP; + /** Action indicating the tool has been moved along the face of the touchscreen. */ + public static final int ACTION_MOVE = MotionEvent.ACTION_MOVE; + /** Action indicating the tool cancelled the current movement. */ + public static final int ACTION_CANCEL = MotionEvent.ACTION_CANCEL; + /** @hide */ + @IntDef(prefix = { "ACTION_" }, value = { + ACTION_UNKNOWN, + ACTION_DOWN, + ACTION_UP, + ACTION_MOVE, + ACTION_CANCEL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Action {} + + private final int mPointerId; + private final @ToolType int mToolType; + private final @Action int mAction; + private final float mX; + private final float mY; + private final float mPressure; + private final float mMajorAxisSize; + + private VirtualTouchEvent(int pointerId, @ToolType int toolType, @Action int action, + float x, float y, float pressure, float majorAxisSize) { + mPointerId = pointerId; + mToolType = toolType; + mAction = action; + mX = x; + mY = y; + mPressure = pressure; + mMajorAxisSize = majorAxisSize; + } + + private VirtualTouchEvent(@NonNull Parcel parcel) { + mPointerId = parcel.readInt(); + mToolType = parcel.readInt(); + mAction = parcel.readInt(); + mX = parcel.readFloat(); + mY = parcel.readFloat(); + mPressure = parcel.readFloat(); + mMajorAxisSize = parcel.readFloat(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mPointerId); + dest.writeInt(mToolType); + dest.writeInt(mAction); + dest.writeFloat(mX); + dest.writeFloat(mY); + dest.writeFloat(mPressure); + dest.writeFloat(mMajorAxisSize); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Returns the pointer id associated with this event. + */ + public int getPointerId() { + return mPointerId; + } + + /** + * Returns the tool type associated with this event. + */ + public @ToolType int getToolType() { + return mToolType; + } + + /** + * Returns the action associated with this event. + */ + public @Action int getAction() { + return mAction; + } + + /** + * Returns the x-axis location associated with this event. + */ + public float getX() { + return mX; + } + + /** + * Returns the y-axis location associated with this event. + */ + public float getY() { + return mY; + } + + /** + * Returns the pressure associated with this event. Returns {@link Float#NaN} if omitted. + */ + public float getPressure() { + return mPressure; + } + + /** + * Returns the major axis size associated with this event. Returns {@link Float#NaN} if omitted. + */ + public float getMajorAxisSize() { + return mMajorAxisSize; + } + + /** + * Builder for {@link VirtualTouchEvent}. + */ + public static final class Builder { + + private @ToolType int mToolType = TOOL_TYPE_UNKNOWN; + private int mPointerId = MotionEvent.INVALID_POINTER_ID; + private @Action int mAction = ACTION_UNKNOWN; + private float mX = Float.NaN; + private float mY = Float.NaN; + private float mPressure = Float.NaN; + private float mMajorAxisSize = Float.NaN; + + /** + * Creates a {@link VirtualTouchEvent} object with the current builder configuration. + */ + public @NonNull VirtualTouchEvent build() { + if (mToolType == TOOL_TYPE_UNKNOWN || mPointerId == MotionEvent.INVALID_POINTER_ID + || mAction == ACTION_UNKNOWN || Float.isNaN(mX) || Float.isNaN(mY)) { + throw new IllegalArgumentException( + "Cannot build virtual touch event with unset required fields"); + } + if ((mToolType == TOOL_TYPE_PALM && mAction != ACTION_CANCEL) + || (mAction == ACTION_CANCEL && mToolType != TOOL_TYPE_PALM)) { + throw new IllegalArgumentException( + "ACTION_CANCEL and TOOL_TYPE_PALM must always appear together"); + } + return new VirtualTouchEvent(mPointerId, mToolType, mAction, mX, mY, mPressure, + mMajorAxisSize); + } + + /** + * Sets the pointer id of the event. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setPointerId(int pointerId) { + mPointerId = pointerId; + return this; + } + + /** + * Sets the tool type of the event. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setToolType(@ToolType int toolType) { + if (toolType != TOOL_TYPE_FINGER && toolType != TOOL_TYPE_PALM) { + throw new IllegalArgumentException("Unsupported touch event tool type"); + } + mToolType = toolType; + return this; + } + + /** + * Sets the action of the event. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setAction(@Action int action) { + if (action != ACTION_DOWN && action != ACTION_UP && action != ACTION_MOVE + && action != ACTION_CANCEL) { + throw new IllegalArgumentException("Unsupported touch event action type"); + } + mAction = action; + return this; + } + + /** + * Sets the x-axis location of the event. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setX(float absX) { + mX = absX; + return this; + } + + /** + * Sets the y-axis location of the event. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setY(float absY) { + mY = absY; + return this; + } + + /** + * Sets the pressure of the event. This field is optional and can be omitted. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setPressure(@FloatRange(from = 0f) float pressure) { + if (pressure < 0f) { + throw new IllegalArgumentException("Touch event pressure cannot be negative"); + } + mPressure = pressure; + return this; + } + + /** + * Sets the major axis size of the event. This field is optional and can be omitted. + * + * @return this builder, to allow for chaining of calls + */ + public @NonNull Builder setMajorAxisSize(@FloatRange(from = 0f) float majorAxisSize) { + if (majorAxisSize < 0f) { + throw new IllegalArgumentException( + "Touch event major axis size cannot be negative"); + } + mMajorAxisSize = majorAxisSize; + return this; + } + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public VirtualTouchEvent createFromParcel(Parcel source) { + return new VirtualTouchEvent(source); + } + public VirtualTouchEvent[] newArray(int size) { + return new VirtualTouchEvent[size]; + } + }; +} diff --git a/core/java/android/hardware/input/VirtualTouchscreen.java b/core/java/android/hardware/input/VirtualTouchscreen.java new file mode 100644 index 000000000000..c8d602acaff6 --- /dev/null +++ b/core/java/android/hardware/input/VirtualTouchscreen.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.companion.virtual.IVirtualDevice; +import android.os.IBinder; +import android.os.RemoteException; + +import java.io.Closeable; + +/** + * A virtual touchscreen representing a touch-based display input mechanism on a remote device. + * + * This registers an InputDevice that is interpreted like a physically-connected device and + * dispatches received events to it. + * + * @hide + */ +@SystemApi +public class VirtualTouchscreen implements Closeable { + + private final IVirtualDevice mVirtualDevice; + private final IBinder mToken; + + /** @hide */ + public VirtualTouchscreen(IVirtualDevice virtualDevice, IBinder token) { + mVirtualDevice = virtualDevice; + mToken = token; + } + + @Override + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void close() { + try { + mVirtualDevice.unregisterInputDevice(mToken); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Sends a touch event to the system. + * + * @param event the event to send + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void sendTouchEvent(@NonNull VirtualTouchEvent event) { + try { + mVirtualDevice.sendTouchEvent(mToken, event); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index b8e50fc6adf2..adb8b86493d5 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -1495,6 +1495,15 @@ public final class MotionEvent extends InputEvent implements Parcelable { */ public static final int TOOL_TYPE_ERASER = 4; + /** + * Tool type constant: The tool is a palm and should be rejected. + * + * @see #getToolType + * + * @hide + */ + public static final int TOOL_TYPE_PALM = 5; + // NOTE: If you add a new tool type here you must also add it to: // native/include/android/input.h diff --git a/core/tests/coretests/src/android/hardware/input/VirtualKeyEventTest.java b/core/tests/coretests/src/android/hardware/input/VirtualKeyEventTest.java new file mode 100644 index 000000000000..37cc9b70dd1b --- /dev/null +++ b/core/tests/coretests/src/android/hardware/input/VirtualKeyEventTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.testng.Assert.assertThrows; + +import android.view.KeyEvent; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VirtualKeyEventTest { + + @Test + public void keyEvent_emptyBuilder() { + assertThrows(IllegalArgumentException.class, () -> new VirtualKeyEvent.Builder().build()); + } + + @Test + public void keyEvent_noKeyCode() { + assertThrows(IllegalArgumentException.class, + () -> new VirtualKeyEvent.Builder().setAction(VirtualKeyEvent.ACTION_DOWN).build()); + } + + @Test + public void keyEvent_noAction() { + assertThrows(IllegalArgumentException.class, + () -> new VirtualKeyEvent.Builder().setKeyCode(KeyEvent.KEYCODE_A).build()); + } + + @Test + public void keyEvent_created() { + final VirtualKeyEvent event = new VirtualKeyEvent.Builder() + .setAction(VirtualKeyEvent.ACTION_DOWN) + .setKeyCode(KeyEvent.KEYCODE_A).build(); + assertWithMessage("Incorrect key code").that(event.getKeyCode()).isEqualTo( + KeyEvent.KEYCODE_A); + assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo( + VirtualKeyEvent.ACTION_DOWN); + } +} diff --git a/core/tests/coretests/src/android/hardware/input/VirtualMouseButtonEventTest.java b/core/tests/coretests/src/android/hardware/input/VirtualMouseButtonEventTest.java new file mode 100644 index 000000000000..789e0bb2ff56 --- /dev/null +++ b/core/tests/coretests/src/android/hardware/input/VirtualMouseButtonEventTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.testng.Assert.assertThrows; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VirtualMouseButtonEventTest { + + @Test + public void buttonEvent_emptyBuilder() { + assertThrows(IllegalArgumentException.class, + () -> new VirtualMouseButtonEvent.Builder().build()); + } + + @Test + public void buttonEvent_noButtonCode() { + assertThrows(IllegalArgumentException.class, () -> new VirtualMouseButtonEvent.Builder() + .setAction(VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE).build()); + } + + @Test + public void buttonEvent_noAction() { + assertThrows(IllegalArgumentException.class, () -> new VirtualMouseButtonEvent.Builder() + .setButtonCode(VirtualMouseButtonEvent.BUTTON_BACK).build()); + } + + @Test + public void buttonEvent_created() { + final VirtualMouseButtonEvent event = new VirtualMouseButtonEvent.Builder() + .setAction(VirtualMouseButtonEvent.ACTION_BUTTON_PRESS) + .setButtonCode(VirtualMouseButtonEvent.BUTTON_BACK).build(); + assertWithMessage("Incorrect button code").that(event.getButtonCode()).isEqualTo( + VirtualMouseButtonEvent.BUTTON_BACK); + assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo( + VirtualMouseButtonEvent.ACTION_BUTTON_PRESS); + } +} diff --git a/core/tests/coretests/src/android/hardware/input/VirtualMouseRelativeEventTest.java b/core/tests/coretests/src/android/hardware/input/VirtualMouseRelativeEventTest.java new file mode 100644 index 000000000000..c0508162869b --- /dev/null +++ b/core/tests/coretests/src/android/hardware/input/VirtualMouseRelativeEventTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import static com.google.common.truth.Truth.assertWithMessage; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VirtualMouseRelativeEventTest { + + @Test + public void relativeEvent_created() { + final VirtualMouseRelativeEvent event = new VirtualMouseRelativeEvent.Builder() + .setRelativeX(-5f) + .setRelativeY(8f).build(); + assertWithMessage("Incorrect x value").that(event.getRelativeX()).isEqualTo(-5f); + assertWithMessage("Incorrect y value").that(event.getRelativeY()).isEqualTo(8f); + } +} diff --git a/core/tests/coretests/src/android/hardware/input/VirtualMouseScrollEventTest.java b/core/tests/coretests/src/android/hardware/input/VirtualMouseScrollEventTest.java new file mode 100644 index 000000000000..2259c740da7e --- /dev/null +++ b/core/tests/coretests/src/android/hardware/input/VirtualMouseScrollEventTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.testng.Assert.assertThrows; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VirtualMouseScrollEventTest { + + @Test + public void scrollEvent_xOutOfRange() { + assertThrows(IllegalArgumentException.class, () -> new VirtualMouseScrollEvent.Builder() + .setXAxisMovement(1.5f) + .setYAxisMovement(1.0f)); + } + + @Test + public void scrollEvent_yOutOfRange() { + assertThrows(IllegalArgumentException.class, () -> new VirtualMouseScrollEvent.Builder() + .setXAxisMovement(0.5f) + .setYAxisMovement(1.1f)); + } + + @Test + public void scrollEvent_created() { + final VirtualMouseScrollEvent event = new VirtualMouseScrollEvent.Builder() + .setXAxisMovement(-1f) + .setYAxisMovement(1f).build(); + assertWithMessage("Incorrect x value").that(event.getXAxisMovement()).isEqualTo(-1f); + assertWithMessage("Incorrect y value").that(event.getYAxisMovement()).isEqualTo(1f); + } +} + diff --git a/core/tests/coretests/src/android/hardware/input/VirtualTouchEventTest.java b/core/tests/coretests/src/android/hardware/input/VirtualTouchEventTest.java new file mode 100644 index 000000000000..3f504a00773c --- /dev/null +++ b/core/tests/coretests/src/android/hardware/input/VirtualTouchEventTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.testng.Assert.assertThrows; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VirtualTouchEventTest { + + @Test + public void touchEvent_emptyBuilder() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder().build()); + } + + @Test + public void touchEvent_noAction() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setY(1f) + .setPointerId(1) + .build()); + } + + @Test + public void touchEvent_noPointerId() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setY(1f) + .build()); + } + + @Test + public void touchEvent_noToolType() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setX(0f) + .setY(1f) + .setPointerId(1) + .build()); + } + + @Test + public void touchEvent_noX() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setY(1f) + .setPointerId(1) + .build()); + } + + + @Test + public void touchEvent_noY() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setPointerId(1) + .build()); + } + + @Test + public void touchEvent_created() { + final VirtualTouchEvent event = new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setY(1f) + .setPointerId(1) + .build(); + assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo( + VirtualTouchEvent.ACTION_DOWN); + assertWithMessage("Incorrect tool type").that(event.getToolType()).isEqualTo( + VirtualTouchEvent.TOOL_TYPE_FINGER); + assertWithMessage("Incorrect x").that(event.getX()).isEqualTo(0f); + assertWithMessage("Incorrect y").that(event.getY()).isEqualTo(1f); + assertWithMessage("Incorrect pointer id").that(event.getPointerId()).isEqualTo(1); + } + + @Test + public void touchEvent_created_withPressureAndAxis() { + final VirtualTouchEvent event = new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setY(1f) + .setPointerId(1) + .setPressure(0.5f) + .setMajorAxisSize(10f) + .build(); + assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo( + VirtualTouchEvent.ACTION_DOWN); + assertWithMessage("Incorrect tool type").that(event.getToolType()).isEqualTo( + VirtualTouchEvent.TOOL_TYPE_FINGER); + assertWithMessage("Incorrect x").that(event.getX()).isEqualTo(0f); + assertWithMessage("Incorrect y").that(event.getY()).isEqualTo(1f); + assertWithMessage("Incorrect pointer id").that(event.getPointerId()).isEqualTo(1); + assertWithMessage("Incorrect pressure").that(event.getPressure()).isEqualTo(0.5f); + assertWithMessage("Incorrect major axis size").that(event.getMajorAxisSize()).isEqualTo( + 10f); + } + + @Test + public void touchEvent_cancelUsedImproperly() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_CANCEL) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setY(1f) + .setPointerId(1) + .build()); + } + + @Test + public void touchEvent_palmUsedImproperly() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_MOVE) + .setToolType(VirtualTouchEvent.TOOL_TYPE_PALM) + .setX(0f) + .setY(1f) + .setPointerId(1) + .build()); + } + + @Test + public void touchEvent_palmAndCancelUsedProperly() { + final VirtualTouchEvent event = new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_CANCEL) + .setToolType(VirtualTouchEvent.TOOL_TYPE_PALM) + .setX(0f) + .setY(1f) + .setPointerId(1) + .setPressure(0.5f) + .setMajorAxisSize(10f) + .build(); + assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo( + VirtualTouchEvent.ACTION_CANCEL); + assertWithMessage("Incorrect tool type").that(event.getToolType()).isEqualTo( + VirtualTouchEvent.TOOL_TYPE_PALM); + assertWithMessage("Incorrect x").that(event.getX()).isEqualTo(0f); + assertWithMessage("Incorrect y").that(event.getY()).isEqualTo(1f); + assertWithMessage("Incorrect pointer id").that(event.getPointerId()).isEqualTo(1); + assertWithMessage("Incorrect pressure").that(event.getPressure()).isEqualTo(0.5f); + assertWithMessage("Incorrect major axis size").that(event.getMajorAxisSize()).isEqualTo( + 10f); + } +} diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java new file mode 100644 index 000000000000..067edcc0b08d --- /dev/null +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2021 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.companion.virtual; + +import android.annotation.NonNull; +import android.graphics.Point; +import android.hardware.input.VirtualKeyEvent; +import android.hardware.input.VirtualMouseButtonEvent; +import android.hardware.input.VirtualMouseRelativeEvent; +import android.hardware.input.VirtualMouseScrollEvent; +import android.hardware.input.VirtualTouchEvent; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.io.PrintWriter; +import java.util.Map; + +/** Controls virtual input devices, including device lifecycle and event dispatch. */ +class InputController { + + private final Object mLock; + + /* Token -> file descriptor associations. */ + @VisibleForTesting + @GuardedBy("mLock") + final Map mInputDeviceFds = new ArrayMap<>(); + + private final NativeWrapper mNativeWrapper; + + InputController(@NonNull Object lock) { + this(lock, new NativeWrapper()); + } + + @VisibleForTesting + InputController(@NonNull Object lock, @NonNull NativeWrapper nativeWrapper) { + mLock = lock; + mNativeWrapper = nativeWrapper; + } + + void close() { + synchronized (mLock) { + for (int fd : mInputDeviceFds.values()) { + mNativeWrapper.closeUinput(fd); + } + mInputDeviceFds.clear(); + } + } + + void createKeyboard(@NonNull String deviceName, + int vendorId, + int productId, + @NonNull IBinder deviceToken) { + final int fd = mNativeWrapper.openUinputKeyboard(deviceName, vendorId, productId); + if (fd < 0) { + throw new RuntimeException( + "A native error occurred when creating keyboard: " + -fd); + } + synchronized (mLock) { + mInputDeviceFds.put(deviceToken, fd); + } + try { + deviceToken.linkToDeath(new BinderDeathRecipient(deviceToken), /* flags= */ 0); + } catch (RemoteException e) { + throw new RuntimeException("Could not create virtual keyboard", e); + } + } + + void createMouse(@NonNull String deviceName, + int vendorId, + int productId, + @NonNull IBinder deviceToken) { + final int fd = mNativeWrapper.openUinputMouse(deviceName, vendorId, productId); + if (fd < 0) { + throw new RuntimeException( + "A native error occurred when creating mouse: " + -fd); + } + synchronized (mLock) { + mInputDeviceFds.put(deviceToken, fd); + } + try { + deviceToken.linkToDeath(new BinderDeathRecipient(deviceToken), /* flags= */ 0); + } catch (RemoteException e) { + throw new RuntimeException("Could not create virtual mouse", e); + } + } + + void createTouchscreen(@NonNull String deviceName, + int vendorId, + int productId, + @NonNull IBinder deviceToken, + @NonNull Point screenSize) { + final int fd = mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId, + screenSize.y, screenSize.x); + if (fd < 0) { + throw new RuntimeException( + "A native error occurred when creating touchscreen: " + -fd); + } + synchronized (mLock) { + mInputDeviceFds.put(deviceToken, fd); + } + try { + deviceToken.linkToDeath(new BinderDeathRecipient(deviceToken), /* flags= */ 0); + } catch (RemoteException e) { + throw new RuntimeException("Could not create virtual touchscreen", e); + } + } + + void unregisterInputDevice(@NonNull IBinder token) { + synchronized (mLock) { + final Integer fd = mInputDeviceFds.remove(token); + if (fd == null) { + throw new IllegalArgumentException( + "Could not unregister input device for given token"); + } + mNativeWrapper.closeUinput(fd); + } + } + + boolean sendKeyEvent(@NonNull IBinder token, @NonNull VirtualKeyEvent event) { + synchronized (mLock) { + final Integer fd = mInputDeviceFds.get(token); + if (fd == null) { + throw new IllegalArgumentException( + "Could not send key event to input device for given token"); + } + return mNativeWrapper.writeKeyEvent(fd, event.getKeyCode(), event.getAction()); + } + } + + boolean sendButtonEvent(@NonNull IBinder token, @NonNull VirtualMouseButtonEvent event) { + synchronized (mLock) { + final Integer fd = mInputDeviceFds.get(token); + if (fd == null) { + throw new IllegalArgumentException( + "Could not send button event to input device for given token"); + } + return mNativeWrapper.writeButtonEvent(fd, event.getButtonCode(), event.getAction()); + } + } + + boolean sendTouchEvent(@NonNull IBinder token, @NonNull VirtualTouchEvent event) { + synchronized (mLock) { + final Integer fd = mInputDeviceFds.get(token); + if (fd == null) { + throw new IllegalArgumentException( + "Could not send touch event to input device for given token"); + } + return mNativeWrapper.writeTouchEvent(fd, event.getPointerId(), event.getToolType(), + event.getAction(), event.getX(), event.getY(), event.getPressure(), + event.getMajorAxisSize()); + } + } + + boolean sendRelativeEvent(@NonNull IBinder token, @NonNull VirtualMouseRelativeEvent event) { + synchronized (mLock) { + final Integer fd = mInputDeviceFds.get(token); + if (fd == null) { + throw new IllegalArgumentException( + "Could not send relative event to input device for given token"); + } + return mNativeWrapper.writeRelativeEvent(fd, event.getRelativeX(), + event.getRelativeY()); + } + } + + boolean sendScrollEvent(@NonNull IBinder token, @NonNull VirtualMouseScrollEvent event) { + synchronized (mLock) { + final Integer fd = mInputDeviceFds.get(token); + if (fd == null) { + throw new IllegalArgumentException( + "Could not send scroll event to input device for given token"); + } + return mNativeWrapper.writeScrollEvent(fd, event.getXAxisMovement(), + event.getYAxisMovement()); + } + } + + public void dump(@NonNull PrintWriter fout) { + fout.println(" InputController: "); + synchronized (mLock) { + fout.println(" Active file descriptors: "); + for (int inputDeviceFd : mInputDeviceFds.values()) { + fout.println(inputDeviceFd); + } + } + } + + private static native int nativeOpenUinputKeyboard(String deviceName, int vendorId, + int productId); + private static native int nativeOpenUinputMouse(String deviceName, int vendorId, + int productId); + private static native int nativeOpenUinputTouchscreen(String deviceName, int vendorId, + int productId, int height, int width); + private static native boolean nativeCloseUinput(int fd); + private static native boolean nativeWriteKeyEvent(int fd, int androidKeyCode, int action); + private static native boolean nativeWriteButtonEvent(int fd, int buttonCode, int action); + private static native boolean nativeWriteTouchEvent(int fd, int pointerId, int toolType, + int action, float locationX, float locationY, float pressure, float majorAxisSize); + private static native boolean nativeWriteRelativeEvent(int fd, float relativeX, + float relativeY); + private static native boolean nativeWriteScrollEvent(int fd, float xAxisMovement, + float yAxisMovement); + + /** Wrapper around the static native methods for tests. */ + @VisibleForTesting + protected static class NativeWrapper { + public int openUinputKeyboard(String deviceName, int vendorId, int productId) { + return nativeOpenUinputKeyboard(deviceName, vendorId, + productId); + } + + public int openUinputMouse(String deviceName, int vendorId, int productId) { + return nativeOpenUinputMouse(deviceName, vendorId, + productId); + } + + public int openUinputTouchscreen(String deviceName, int vendorId, int productId, int height, + int width) { + return nativeOpenUinputTouchscreen(deviceName, vendorId, + productId, height, width); + } + + public boolean closeUinput(int fd) { + return nativeCloseUinput(fd); + } + + public boolean writeKeyEvent(int fd, int androidKeyCode, int action) { + return nativeWriteKeyEvent(fd, androidKeyCode, action); + } + + public boolean writeButtonEvent(int fd, int buttonCode, int action) { + return nativeWriteButtonEvent(fd, buttonCode, action); + } + + public boolean writeTouchEvent(int fd, int pointerId, int toolType, int action, + float locationX, float locationY, float pressure, float majorAxisSize) { + return nativeWriteTouchEvent(fd, pointerId, toolType, + action, locationX, locationY, + pressure, majorAxisSize); + } + + public boolean writeRelativeEvent(int fd, float relativeX, float relativeY) { + return nativeWriteRelativeEvent(fd, relativeX, relativeY); + } + + public boolean writeScrollEvent(int fd, float xAxisMovement, float yAxisMovement) { + return nativeWriteScrollEvent(fd, xAxisMovement, + yAxisMovement); + } + } + + private final class BinderDeathRecipient implements IBinder.DeathRecipient { + + private final IBinder mDeviceToken; + + BinderDeathRecipient(IBinder deviceToken) { + mDeviceToken = deviceToken; + } + + @Override + public void binderDied() { + unregisterInputDevice(mDeviceToken); + } + } +} diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java new file mode 100644 index 000000000000..022da4361be4 --- /dev/null +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2021 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.companion.virtual; + +import static android.view.WindowManager.LayoutParams.FLAG_SECURE; +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; + +import android.annotation.NonNull; +import android.companion.AssociationInfo; +import android.companion.virtual.IVirtualDevice; +import android.content.Context; +import android.graphics.Point; +import android.hardware.input.VirtualKeyEvent; +import android.hardware.input.VirtualMouseButtonEvent; +import android.hardware.input.VirtualMouseRelativeEvent; +import android.hardware.input.VirtualMouseScrollEvent; +import android.hardware.input.VirtualTouchEvent; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.window.DisplayWindowPolicyController; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + + +final class VirtualDeviceImpl extends IVirtualDevice.Stub + implements IBinder.DeathRecipient { + + private final Object mVirtualDeviceLock = new Object(); + + private final Context mContext; + private final AssociationInfo mAssociationInfo; + private final int mOwnerUid; + private final GenericWindowPolicyController mGenericWindowPolicyController; + private final InputController mInputController; + @VisibleForTesting + final List mVirtualDisplayIds = new ArrayList<>(); + private final OnDeviceCloseListener mListener; + + VirtualDeviceImpl(Context context, AssociationInfo associationInfo, + IBinder token, int ownerUid, OnDeviceCloseListener listener) { + this(context, associationInfo, token, ownerUid, /* inputController= */ null, listener); + } + + @VisibleForTesting + VirtualDeviceImpl(Context context, AssociationInfo associationInfo, IBinder token, + int ownerUid, InputController inputController, OnDeviceCloseListener listener) { + mContext = context; + mAssociationInfo = associationInfo; + mGenericWindowPolicyController = new GenericWindowPolicyController(FLAG_SECURE, + SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); + mOwnerUid = ownerUid; + if (inputController == null) { + mInputController = new InputController(mVirtualDeviceLock); + } else { + mInputController = inputController; + } + mListener = listener; + try { + token.linkToDeath(this, 0); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public int getAssociationId() { + return mAssociationInfo.getId(); + } + + @Override // Binder call + public void close() { + mListener.onClose(mAssociationInfo.getId()); + mInputController.close(); + } + + @Override + public void binderDied() { + close(); + } + + @Override // Binder call + public void createVirtualKeyboard( + int displayId, + @NonNull String deviceName, + int vendorId, + int productId, + @NonNull IBinder deviceToken) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CREATE_VIRTUAL_DEVICE, + "Permission required to create a virtual keyboard"); + synchronized (mVirtualDeviceLock) { + if (!mVirtualDisplayIds.contains(displayId)) { + throw new SecurityException( + "Cannot create a virtual keyboard for a display not associated with " + + "this virtual device"); + } + } + final long token = Binder.clearCallingIdentity(); + try { + mInputController.createKeyboard(deviceName, vendorId, productId, deviceToken); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override // Binder call + public void createVirtualMouse( + int displayId, + @NonNull String deviceName, + int vendorId, + int productId, + @NonNull IBinder deviceToken) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CREATE_VIRTUAL_DEVICE, + "Permission required to create a virtual mouse"); + synchronized (mVirtualDeviceLock) { + if (!mVirtualDisplayIds.contains(displayId)) { + throw new SecurityException( + "Cannot create a virtual mouse for a display not associated with this " + + "virtual device"); + } + } + final long token = Binder.clearCallingIdentity(); + try { + mInputController.createMouse(deviceName, vendorId, productId, deviceToken); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override // Binder call + public void createVirtualTouchscreen( + int displayId, + @NonNull String deviceName, + int vendorId, + int productId, + @NonNull IBinder deviceToken, + @NonNull Point screenSize) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CREATE_VIRTUAL_DEVICE, + "Permission required to create a virtual touchscreen"); + synchronized (mVirtualDeviceLock) { + if (!mVirtualDisplayIds.contains(displayId)) { + throw new SecurityException( + "Cannot create a virtual touchscreen for a display not associated with " + + "this virtual device"); + } + } + final long token = Binder.clearCallingIdentity(); + try { + mInputController.createTouchscreen(deviceName, vendorId, productId, + deviceToken, screenSize); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override // Binder call + public void unregisterInputDevice(IBinder token) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CREATE_VIRTUAL_DEVICE, + "Permission required to unregister this input device"); + + final long binderToken = Binder.clearCallingIdentity(); + try { + mInputController.unregisterInputDevice(token); + } finally { + Binder.restoreCallingIdentity(binderToken); + } + } + + @Override // Binder call + public boolean sendKeyEvent(IBinder token, VirtualKeyEvent event) { + final long binderToken = Binder.clearCallingIdentity(); + try { + return mInputController.sendKeyEvent(token, event); + } finally { + Binder.restoreCallingIdentity(binderToken); + } + } + + @Override // Binder call + public boolean sendButtonEvent(IBinder token, VirtualMouseButtonEvent event) { + final long binderToken = Binder.clearCallingIdentity(); + try { + return mInputController.sendButtonEvent(token, event); + } finally { + Binder.restoreCallingIdentity(binderToken); + } + } + + @Override // Binder call + public boolean sendTouchEvent(IBinder token, VirtualTouchEvent event) { + final long binderToken = Binder.clearCallingIdentity(); + try { + return mInputController.sendTouchEvent(token, event); + } finally { + Binder.restoreCallingIdentity(binderToken); + } + } + + @Override // Binder call + public boolean sendRelativeEvent(IBinder token, VirtualMouseRelativeEvent event) { + final long binderToken = Binder.clearCallingIdentity(); + try { + return mInputController.sendRelativeEvent(token, event); + } finally { + Binder.restoreCallingIdentity(binderToken); + } + } + + @Override // Binder call + public boolean sendScrollEvent(IBinder token, VirtualMouseScrollEvent event) { + final long binderToken = Binder.clearCallingIdentity(); + try { + return mInputController.sendScrollEvent(token, event); + } finally { + Binder.restoreCallingIdentity(binderToken); + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + fout.println(" VirtualDevice: "); + fout.println(" mVirtualDisplayIds: "); + synchronized (mVirtualDeviceLock) { + for (int id : mVirtualDisplayIds) { + fout.println(" " + id); + } + } + mInputController.dump(fout); + } + + DisplayWindowPolicyController onVirtualDisplayCreatedLocked(int displayId) { + if (mVirtualDisplayIds.contains(displayId)) { + throw new IllegalStateException( + "Virtual device already have a virtual display with ID " + displayId); + } + mVirtualDisplayIds.add(displayId); + return mGenericWindowPolicyController; + } + + void onVirtualDisplayRemovedLocked(int displayId) { + if (!mVirtualDisplayIds.contains(displayId)) { + throw new IllegalStateException( + "Virtual device doesn't have a virtual display with ID " + displayId); + } + mVirtualDisplayIds.remove(displayId); + } + + int getOwnerUid() { + return mOwnerUid; + } + + interface OnDeviceCloseListener { + void onClose(int associationId); + } +} diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 27426089f409..46e75f75ea9f 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -16,9 +16,6 @@ package com.android.server.companion.virtual; -import static android.view.WindowManager.LayoutParams.FLAG_SECURE; -import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; - import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -42,7 +39,6 @@ import com.android.server.SystemService; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -52,6 +48,7 @@ public class VirtualDeviceManagerService extends SystemService { private static final boolean DEBUG = false; private static final String LOG_TAG = "VirtualDeviceManagerService"; + private final Object mVirtualDeviceManagerLock = new Object(); private final VirtualDeviceManagerImpl mImpl; @@ -130,64 +127,9 @@ public class VirtualDeviceManagerService extends SystemService { } } - private class VirtualDeviceImpl extends IVirtualDevice.Stub implements IBinder.DeathRecipient { - - private final AssociationInfo mAssociationInfo; - private final int mOwnerUid; - private final GenericWindowPolicyController mGenericWindowPolicyController; - private final ArrayList mDisplayIds = new ArrayList<>(); - - private VirtualDeviceImpl(int ownerUid, IBinder token, AssociationInfo associationInfo) { - mOwnerUid = ownerUid; - mAssociationInfo = associationInfo; - mGenericWindowPolicyController = new GenericWindowPolicyController(FLAG_SECURE, - SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); - try { - token.linkToDeath(this, 0); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - mVirtualDevices.put(associationInfo.getId(), this); - } - - @Override - public int getAssociationId() { - return mAssociationInfo.getId(); - } - - @Override - public void close() { - synchronized (mVirtualDeviceManagerLock) { - mVirtualDevices.remove(mAssociationInfo.getId()); - } - } - - @Override - public void binderDied() { - close(); - } - - DisplayWindowPolicyController onVirtualDisplayCreatedLocked(int displayId) { - if (mDisplayIds.contains(displayId)) { - throw new IllegalStateException( - "Virtual device already have a virtual display with ID " + displayId); - } - mDisplayIds.add(displayId); - return mGenericWindowPolicyController; - } - - void onVirtualDisplayRemovedLocked(int displayId) { - if (!mDisplayIds.contains(displayId)) { - throw new IllegalStateException( - "Virtual device doesn't have a virtual display with ID " + displayId); - } - mDisplayIds.remove(displayId); - } - } - class VirtualDeviceManagerImpl extends IVirtualDeviceManager.Stub { - @Override + @Override // Binder call public IVirtualDevice createVirtualDevice( IBinder token, String packageName, int associationId) { getContext().enforceCallingOrSelfPermission( @@ -209,7 +151,18 @@ public class VirtualDeviceManagerService extends SystemService { "Virtual device for association ID " + associationId + " already exists"); } - return new VirtualDeviceImpl(callingUid, token, associationInfo); + VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(), + associationInfo, token, callingUid, + new VirtualDeviceImpl.OnDeviceCloseListener() { + @Override + public void onClose(int associationId) { + synchronized (mVirtualDeviceManagerLock) { + mVirtualDevices.remove(associationId); + } + } + }); + mVirtualDevices.put(associationInfo.getId(), virtualDevice); + return virtualDevice; } } @@ -254,8 +207,7 @@ public class VirtualDeviceManagerService extends SystemService { fout.println("Created virtual devices: "); synchronized (mVirtualDeviceManagerLock) { for (int i = 0; i < mVirtualDevices.size(); i++) { - VirtualDeviceImpl virtualDevice = mVirtualDevices.valueAt(i); - fout.printf("%d: %s\n", mVirtualDevices.keyAt(i), virtualDevice); + mVirtualDevices.valueAt(i).dump(fd, fout, args); } } } @@ -290,7 +242,7 @@ public class VirtualDeviceManagerService extends SystemService { synchronized (mVirtualDeviceManagerLock) { int size = mVirtualDevices.size(); for (int i = 0; i < size; i++) { - if (mVirtualDevices.valueAt(i).mOwnerUid == uid) { + if (mVirtualDevices.valueAt(i).getOwnerUid() == uid) { return true; } } diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index f72f2cc156ec..f5eb0a7a34d3 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -35,6 +35,7 @@ cc_library_static { "com_android_server_am_BatteryStatsService.cpp", "com_android_server_biometrics_SurfaceToNativeHandleConverter.cpp", "com_android_server_ConsumerIrService.cpp", + "com_android_server_companion_virtual_InputController.cpp", "com_android_server_devicepolicy_CryptoTestHelper.cpp", "com_android_server_connectivity_Vpn.cpp", "com_android_server_gpu_GpuService.cpp", diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp new file mode 100644 index 000000000000..43018a900f4c --- /dev/null +++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2021 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. + */ + +#define LOG_TAG "InputController" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace android { + +enum class DeviceType { + KEYBOARD, + MOUSE, + TOUCHSCREEN, +}; + +enum class UinputAction { + RELEASE = 0, + PRESS = 1, + MOVE = 2, + CANCEL = 3, +}; + +static std::map BUTTON_ACTION_MAPPING = { + {AMOTION_EVENT_ACTION_BUTTON_PRESS, UinputAction::PRESS}, + {AMOTION_EVENT_ACTION_BUTTON_RELEASE, UinputAction::RELEASE}, +}; + +static std::map KEY_ACTION_MAPPING = { + {AKEY_EVENT_ACTION_DOWN, UinputAction::PRESS}, + {AKEY_EVENT_ACTION_UP, UinputAction::RELEASE}, +}; + +static std::map TOUCH_ACTION_MAPPING = { + {AMOTION_EVENT_ACTION_DOWN, UinputAction::PRESS}, + {AMOTION_EVENT_ACTION_UP, UinputAction::RELEASE}, + {AMOTION_EVENT_ACTION_MOVE, UinputAction::MOVE}, + {AMOTION_EVENT_ACTION_CANCEL, UinputAction::CANCEL}, +}; + +// Button code mapping from https://source.android.com/devices/input/touch-devices +static std::map BUTTON_CODE_MAPPING = { + {AMOTION_EVENT_BUTTON_PRIMARY, BTN_LEFT}, {AMOTION_EVENT_BUTTON_SECONDARY, BTN_RIGHT}, + {AMOTION_EVENT_BUTTON_TERTIARY, BTN_MIDDLE}, {AMOTION_EVENT_BUTTON_BACK, BTN_BACK}, + {AMOTION_EVENT_BUTTON_FORWARD, BTN_FORWARD}, +}; + +// Tool type mapping from https://source.android.com/devices/input/touch-devices +static std::map TOOL_TYPE_MAPPING = { + {AMOTION_EVENT_TOOL_TYPE_FINGER, MT_TOOL_FINGER}, + {AMOTION_EVENT_TOOL_TYPE_PALM, MT_TOOL_PALM}, +}; + +// Keycode mapping from https://source.android.com/devices/input/keyboard-devices +static std::map KEY_CODE_MAPPING = { + {AKEYCODE_0, KEY_0}, + {AKEYCODE_1, KEY_1}, + {AKEYCODE_2, KEY_2}, + {AKEYCODE_3, KEY_3}, + {AKEYCODE_4, KEY_4}, + {AKEYCODE_5, KEY_5}, + {AKEYCODE_6, KEY_6}, + {AKEYCODE_7, KEY_7}, + {AKEYCODE_8, KEY_8}, + {AKEYCODE_9, KEY_9}, + {AKEYCODE_A, KEY_A}, + {AKEYCODE_B, KEY_B}, + {AKEYCODE_C, KEY_C}, + {AKEYCODE_D, KEY_D}, + {AKEYCODE_E, KEY_E}, + {AKEYCODE_F, KEY_F}, + {AKEYCODE_G, KEY_G}, + {AKEYCODE_H, KEY_H}, + {AKEYCODE_I, KEY_I}, + {AKEYCODE_J, KEY_J}, + {AKEYCODE_K, KEY_K}, + {AKEYCODE_L, KEY_L}, + {AKEYCODE_M, KEY_M}, + {AKEYCODE_N, KEY_N}, + {AKEYCODE_O, KEY_O}, + {AKEYCODE_P, KEY_P}, + {AKEYCODE_Q, KEY_Q}, + {AKEYCODE_R, KEY_R}, + {AKEYCODE_S, KEY_S}, + {AKEYCODE_T, KEY_T}, + {AKEYCODE_U, KEY_U}, + {AKEYCODE_V, KEY_V}, + {AKEYCODE_W, KEY_W}, + {AKEYCODE_X, KEY_X}, + {AKEYCODE_Y, KEY_Y}, + {AKEYCODE_Z, KEY_Z}, + {AKEYCODE_GRAVE, KEY_GRAVE}, + {AKEYCODE_MINUS, KEY_MINUS}, + {AKEYCODE_EQUALS, KEY_EQUAL}, + {AKEYCODE_LEFT_BRACKET, KEY_LEFTBRACE}, + {AKEYCODE_RIGHT_BRACKET, KEY_RIGHTBRACE}, + {AKEYCODE_BACKSLASH, KEY_BACKSLASH}, + {AKEYCODE_SEMICOLON, KEY_SEMICOLON}, + {AKEYCODE_APOSTROPHE, KEY_APOSTROPHE}, + {AKEYCODE_COMMA, KEY_COMMA}, + {AKEYCODE_PERIOD, KEY_DOT}, + {AKEYCODE_SLASH, KEY_SLASH}, + {AKEYCODE_ALT_LEFT, KEY_LEFTALT}, + {AKEYCODE_ALT_RIGHT, KEY_RIGHTALT}, + {AKEYCODE_CTRL_LEFT, KEY_LEFTCTRL}, + {AKEYCODE_CTRL_RIGHT, KEY_RIGHTCTRL}, + {AKEYCODE_SHIFT_LEFT, KEY_LEFTSHIFT}, + {AKEYCODE_SHIFT_RIGHT, KEY_RIGHTSHIFT}, + {AKEYCODE_META_LEFT, KEY_LEFTMETA}, + {AKEYCODE_META_RIGHT, KEY_RIGHTMETA}, + {AKEYCODE_CAPS_LOCK, KEY_CAPSLOCK}, + {AKEYCODE_SCROLL_LOCK, KEY_SCROLLLOCK}, + {AKEYCODE_NUM_LOCK, KEY_NUMLOCK}, + {AKEYCODE_ENTER, KEY_ENTER}, + {AKEYCODE_TAB, KEY_TAB}, + {AKEYCODE_SPACE, KEY_SPACE}, + {AKEYCODE_DPAD_DOWN, KEY_DOWN}, + {AKEYCODE_DPAD_UP, KEY_UP}, + {AKEYCODE_DPAD_LEFT, KEY_LEFT}, + {AKEYCODE_DPAD_RIGHT, KEY_RIGHT}, + {AKEYCODE_MOVE_END, KEY_END}, + {AKEYCODE_MOVE_HOME, KEY_HOME}, + {AKEYCODE_PAGE_DOWN, KEY_PAGEDOWN}, + {AKEYCODE_PAGE_UP, KEY_PAGEUP}, + {AKEYCODE_DEL, KEY_BACKSPACE}, + {AKEYCODE_FORWARD_DEL, KEY_DELETE}, + {AKEYCODE_INSERT, KEY_INSERT}, + {AKEYCODE_ESCAPE, KEY_ESC}, + {AKEYCODE_BREAK, KEY_PAUSE}, + {AKEYCODE_F1, KEY_F1}, + {AKEYCODE_F2, KEY_F2}, + {AKEYCODE_F3, KEY_F3}, + {AKEYCODE_F4, KEY_F4}, + {AKEYCODE_F5, KEY_F5}, + {AKEYCODE_F6, KEY_F6}, + {AKEYCODE_F7, KEY_F7}, + {AKEYCODE_F8, KEY_F8}, + {AKEYCODE_F9, KEY_F9}, + {AKEYCODE_F10, KEY_F10}, + {AKEYCODE_F11, KEY_F11}, + {AKEYCODE_F12, KEY_F12}, + {AKEYCODE_BACK, KEY_BACK}, + {AKEYCODE_FORWARD, KEY_FORWARD}, + {AKEYCODE_NUMPAD_1, KEY_KP1}, + {AKEYCODE_NUMPAD_2, KEY_KP2}, + {AKEYCODE_NUMPAD_3, KEY_KP3}, + {AKEYCODE_NUMPAD_4, KEY_KP4}, + {AKEYCODE_NUMPAD_5, KEY_KP5}, + {AKEYCODE_NUMPAD_6, KEY_KP6}, + {AKEYCODE_NUMPAD_7, KEY_KP7}, + {AKEYCODE_NUMPAD_8, KEY_KP8}, + {AKEYCODE_NUMPAD_9, KEY_KP9}, + {AKEYCODE_NUMPAD_0, KEY_KP0}, + {AKEYCODE_NUMPAD_ADD, KEY_KPPLUS}, + {AKEYCODE_NUMPAD_SUBTRACT, KEY_KPMINUS}, + {AKEYCODE_NUMPAD_MULTIPLY, KEY_KPASTERISK}, + {AKEYCODE_NUMPAD_DIVIDE, KEY_KPSLASH}, + {AKEYCODE_NUMPAD_DOT, KEY_KPDOT}, + {AKEYCODE_NUMPAD_ENTER, KEY_KPENTER}, + {AKEYCODE_NUMPAD_EQUALS, KEY_KPEQUAL}, + {AKEYCODE_NUMPAD_COMMA, KEY_KPCOMMA}, +}; + +/** Creates a new uinput device and assigns a file descriptor. */ +static int openUinput(const char* readableName, jint vendorId, jint productId, + DeviceType deviceType, jint screenHeight, jint screenWidth) { + android::base::unique_fd fd(TEMP_FAILURE_RETRY(::open("/dev/uinput", O_WRONLY | O_NONBLOCK))); + if (fd < 0) { + ALOGE("Error creating uinput device: %s", strerror(errno)); + return -errno; + } + + ioctl(fd, UI_SET_EVBIT, EV_KEY); + ioctl(fd, UI_SET_EVBIT, EV_SYN); + switch (deviceType) { + case DeviceType::KEYBOARD: + for (const auto& [ignored, keyCode] : KEY_CODE_MAPPING) { + ioctl(fd, UI_SET_KEYBIT, keyCode); + } + break; + case DeviceType::MOUSE: + ioctl(fd, UI_SET_EVBIT, EV_REL); + ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); + ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT); + ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE); + ioctl(fd, UI_SET_KEYBIT, BTN_BACK); + ioctl(fd, UI_SET_KEYBIT, BTN_FORWARD); + ioctl(fd, UI_SET_RELBIT, REL_X); + ioctl(fd, UI_SET_RELBIT, REL_Y); + ioctl(fd, UI_SET_RELBIT, REL_WHEEL); + ioctl(fd, UI_SET_RELBIT, REL_HWHEEL); + break; + case DeviceType::TOUCHSCREEN: + ioctl(fd, UI_SET_EVBIT, EV_ABS); + ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH); + ioctl(fd, UI_SET_ABSBIT, ABS_MT_SLOT); + ioctl(fd, UI_SET_ABSBIT, ABS_MT_POSITION_X); + ioctl(fd, UI_SET_ABSBIT, ABS_MT_POSITION_Y); + ioctl(fd, UI_SET_ABSBIT, ABS_MT_TRACKING_ID); + ioctl(fd, UI_SET_ABSBIT, ABS_MT_TOOL_TYPE); + ioctl(fd, UI_SET_ABSBIT, ABS_MT_TOUCH_MAJOR); + ioctl(fd, UI_SET_ABSBIT, ABS_MT_PRESSURE); + ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT); + } + + int version; + if (ioctl(fd, UI_GET_VERSION, &version) == 0 && version >= 5) { + uinput_setup setup; + memset(&setup, 0, sizeof(setup)); + strlcpy(setup.name, readableName, UINPUT_MAX_NAME_SIZE); + setup.id.version = 1; + setup.id.bustype = BUS_VIRTUAL; + setup.id.vendor = vendorId; + setup.id.product = productId; + if (deviceType == DeviceType::TOUCHSCREEN) { + uinput_abs_setup xAbsSetup; + xAbsSetup.code = ABS_MT_POSITION_X; + xAbsSetup.absinfo.maximum = screenWidth - 1; + xAbsSetup.absinfo.minimum = 0; + ioctl(fd, UI_ABS_SETUP, xAbsSetup); + uinput_abs_setup yAbsSetup; + yAbsSetup.code = ABS_MT_POSITION_Y; + yAbsSetup.absinfo.maximum = screenHeight - 1; + yAbsSetup.absinfo.minimum = 0; + ioctl(fd, UI_ABS_SETUP, yAbsSetup); + uinput_abs_setup majorAbsSetup; + majorAbsSetup.code = ABS_MT_TOUCH_MAJOR; + majorAbsSetup.absinfo.maximum = screenWidth - 1; + majorAbsSetup.absinfo.minimum = 0; + ioctl(fd, UI_ABS_SETUP, majorAbsSetup); + uinput_abs_setup pressureAbsSetup; + pressureAbsSetup.code = ABS_MT_PRESSURE; + pressureAbsSetup.absinfo.maximum = 255; + pressureAbsSetup.absinfo.minimum = 0; + ioctl(fd, UI_ABS_SETUP, pressureAbsSetup); + } + if (ioctl(fd, UI_DEV_SETUP, &setup) != 0) { + ALOGE("Error creating uinput device: %s", strerror(errno)); + return -errno; + } + } else { + // UI_DEV_SETUP was not introduced until version 5. Try setting up manually. + uinput_user_dev fallback; + memset(&fallback, 0, sizeof(fallback)); + strlcpy(fallback.name, readableName, UINPUT_MAX_NAME_SIZE); + fallback.id.version = 1; + fallback.id.bustype = BUS_VIRTUAL; + fallback.id.vendor = vendorId; + fallback.id.product = productId; + if (deviceType == DeviceType::TOUCHSCREEN) { + fallback.absmin[ABS_MT_POSITION_X] = 0; + fallback.absmax[ABS_MT_POSITION_X] = screenWidth - 1; + fallback.absmin[ABS_MT_POSITION_Y] = 0; + fallback.absmax[ABS_MT_POSITION_Y] = screenHeight - 1; + fallback.absmin[ABS_MT_TOUCH_MAJOR] = 0; + fallback.absmax[ABS_MT_TOUCH_MAJOR] = screenWidth - 1; + fallback.absmin[ABS_MT_PRESSURE] = 0; + fallback.absmax[ABS_MT_PRESSURE] = 255; + } + if (TEMP_FAILURE_RETRY(write(fd, &fallback, sizeof(fallback))) != sizeof(fallback)) { + ALOGE("Error creating uinput device: %s", strerror(errno)); + return -errno; + } + } + + if (ioctl(fd, UI_DEV_CREATE) != 0) { + ALOGE("Error creating uinput device: %s", strerror(errno)); + return -errno; + } + + return fd.release(); +} + +static int openUinputJni(JNIEnv* env, jstring name, jint vendorId, jint productId, + DeviceType deviceType, int screenHeight, int screenWidth) { + ScopedUtfChars readableName(env, name); + return openUinput(readableName.c_str(), vendorId, productId, deviceType, screenHeight, + screenWidth); +} + +static int nativeOpenUinputKeyboard(JNIEnv* env, jobject thiz, jstring name, jint vendorId, + jint productId) { + return openUinputJni(env, name, vendorId, productId, DeviceType::KEYBOARD, /* screenHeight */ 0, + /* screenWidth */ 0); +} + +static int nativeOpenUinputMouse(JNIEnv* env, jobject thiz, jstring name, jint vendorId, + jint productId) { + return openUinputJni(env, name, vendorId, productId, DeviceType::MOUSE, /* screenHeight */ 0, + /* screenWidth */ 0); +} + +static int nativeOpenUinputTouchscreen(JNIEnv* env, jobject thiz, jstring name, jint vendorId, + jint productId, jint height, jint width) { + return openUinputJni(env, name, vendorId, productId, DeviceType::TOUCHSCREEN, height, width); +} + +static bool nativeCloseUinput(JNIEnv* env, jobject thiz, jint fd) { + ioctl(fd, UI_DEV_DESTROY); + return close(fd); +} + +static bool writeInputEvent(int fd, uint16_t type, uint16_t code, int32_t value) { + struct input_event ev = {.type = type, .code = code, .value = value}; + return TEMP_FAILURE_RETRY(write(fd, &ev, sizeof(struct input_event))) == sizeof(ev); +} + +static bool nativeWriteKeyEvent(JNIEnv* env, jobject thiz, jint fd, jint androidKeyCode, + jint action) { + auto keyCodeIterator = KEY_CODE_MAPPING.find(androidKeyCode); + if (keyCodeIterator == KEY_CODE_MAPPING.end()) { + ALOGE("No supportive native keycode for androidKeyCode %d", androidKeyCode); + return false; + } + auto actionIterator = KEY_ACTION_MAPPING.find(action); + if (actionIterator == KEY_ACTION_MAPPING.end()) { + return false; + } + if (!writeInputEvent(fd, EV_KEY, static_cast(keyCodeIterator->second), + static_cast(actionIterator->second))) { + return false; + } + if (!writeInputEvent(fd, EV_SYN, SYN_REPORT, 0)) { + return false; + } + return true; +} + +static bool nativeWriteButtonEvent(JNIEnv* env, jobject thiz, jint fd, jint buttonCode, + jint action) { + auto buttonCodeIterator = BUTTON_CODE_MAPPING.find(buttonCode); + if (buttonCodeIterator == BUTTON_CODE_MAPPING.end()) { + return false; + } + auto actionIterator = BUTTON_ACTION_MAPPING.find(action); + if (actionIterator == BUTTON_ACTION_MAPPING.end()) { + return false; + } + if (!writeInputEvent(fd, EV_KEY, static_cast(buttonCodeIterator->second), + static_cast(actionIterator->second))) { + return false; + } + if (!writeInputEvent(fd, EV_SYN, SYN_REPORT, 0)) { + return false; + } + return true; +} + +static bool nativeWriteTouchEvent(JNIEnv* env, jobject thiz, jint fd, jint pointerId, jint toolType, + jint action, jfloat locationX, jfloat locationY, jfloat pressure, + jfloat majorAxisSize) { + if (!writeInputEvent(fd, EV_ABS, ABS_MT_SLOT, pointerId)) { + return false; + } + auto toolTypeIterator = TOOL_TYPE_MAPPING.find(toolType); + if (toolTypeIterator == TOOL_TYPE_MAPPING.end()) { + return false; + } + if (toolType != -1) { + if (!writeInputEvent(fd, EV_ABS, ABS_MT_TOOL_TYPE, + static_cast(toolTypeIterator->second))) { + return false; + } + } + auto actionIterator = TOUCH_ACTION_MAPPING.find(action); + if (actionIterator == TOUCH_ACTION_MAPPING.end()) { + return false; + } + UinputAction uinputAction = actionIterator->second; + if (uinputAction == UinputAction::PRESS || uinputAction == UinputAction::RELEASE) { + if (!writeInputEvent(fd, EV_KEY, BTN_TOUCH, static_cast(uinputAction))) { + return false; + } + if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID, + static_cast(uinputAction == UinputAction::PRESS ? pointerId + : -1))) { + return false; + } + } + if (!writeInputEvent(fd, EV_ABS, ABS_MT_POSITION_X, locationX)) { + return false; + } + if (!writeInputEvent(fd, EV_ABS, ABS_MT_POSITION_Y, locationY)) { + return false; + } + if (!isnan(pressure)) { + if (!writeInputEvent(fd, EV_ABS, ABS_MT_PRESSURE, pressure)) { + return false; + } + } + if (!isnan(majorAxisSize)) { + if (!writeInputEvent(fd, EV_ABS, ABS_MT_TOUCH_MAJOR, majorAxisSize)) { + return false; + } + } + return writeInputEvent(fd, EV_SYN, SYN_REPORT, 0); +} + +static bool nativeWriteRelativeEvent(JNIEnv* env, jobject thiz, jint fd, jfloat relativeX, + jfloat relativeY) { + return writeInputEvent(fd, EV_REL, REL_X, relativeX) && + writeInputEvent(fd, EV_REL, REL_Y, relativeY) && + writeInputEvent(fd, EV_SYN, SYN_REPORT, 0); +} + +static bool nativeWriteScrollEvent(JNIEnv* env, jobject thiz, jint fd, jfloat xAxisMovement, + jfloat yAxisMovement) { + return writeInputEvent(fd, EV_REL, REL_HWHEEL, xAxisMovement) && + writeInputEvent(fd, EV_REL, REL_WHEEL, yAxisMovement) && + writeInputEvent(fd, EV_SYN, SYN_REPORT, 0); +} + +static JNINativeMethod methods[] = { + {"nativeOpenUinputKeyboard", "(Ljava/lang/String;II)I", (void*)nativeOpenUinputKeyboard}, + {"nativeOpenUinputMouse", "(Ljava/lang/String;II)I", (void*)nativeOpenUinputMouse}, + {"nativeOpenUinputTouchscreen", "(Ljava/lang/String;IIII)I", + (void*)nativeOpenUinputTouchscreen}, + {"nativeCloseUinput", "(I)Z", (void*)nativeCloseUinput}, + {"nativeWriteKeyEvent", "(III)Z", (void*)nativeWriteKeyEvent}, + {"nativeWriteButtonEvent", "(III)Z", (void*)nativeWriteButtonEvent}, + {"nativeWriteTouchEvent", "(IIIIFFFF)Z", (void*)nativeWriteTouchEvent}, + {"nativeWriteRelativeEvent", "(IFF)Z", (void*)nativeWriteRelativeEvent}, + {"nativeWriteScrollEvent", "(IFF)Z", (void*)nativeWriteScrollEvent}, +}; + +int register_android_server_companion_virtual_InputController(JNIEnv* env) { + return jniRegisterNativeMethods(env, "com/android/server/companion/virtual/InputController", + methods, NELEM(methods)); +} + +} // namespace android \ No newline at end of file diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index ff61abc4ff7f..d339ef1154c5 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -63,6 +63,7 @@ int register_android_server_FaceService(JNIEnv* env); int register_android_server_GpuService(JNIEnv* env); int register_android_server_stats_pull_StatsPullAtomService(JNIEnv* env); int register_android_server_sensor_SensorService(JavaVM* vm, JNIEnv* env); +int register_android_server_companion_virtual_InputController(JNIEnv* env); }; using namespace android; @@ -119,5 +120,6 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_GpuService(env); register_android_server_stats_pull_StatsPullAtomService(env); register_android_server_sensor_SensorService(vm, env); + register_android_server_companion_virtual_InputController(env); return JNI_VERSION_1_4; } diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index e3c60fdfc697..c3a364e723fb 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -32,6 +32,7 @@ android_test { "services.appwidget", "services.autofill", "services.backup", + "services.companion", "services.core", "services.devicepolicy", "services.net", diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java new file mode 100644 index 000000000000..c7c0756bc0d0 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2021 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.companion.virtual; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertThrows; + +import android.Manifest; +import android.content.Context; +import android.content.ContextWrapper; +import android.graphics.Point; +import android.hardware.display.DisplayManagerInternal; +import android.hardware.input.VirtualKeyEvent; +import android.hardware.input.VirtualMouseButtonEvent; +import android.hardware.input.VirtualMouseRelativeEvent; +import android.hardware.input.VirtualMouseScrollEvent; +import android.hardware.input.VirtualTouchEvent; +import android.os.Binder; +import android.platform.test.annotations.Presubmit; +import android.view.KeyEvent; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.LocalServices; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class VirtualDeviceManagerServiceTest { + + private static final String DEVICE_NAME = "device name"; + private static final int DISPLAY_ID = 2; + private static final int PRODUCT_ID = 10; + private static final int VENDOR_ID = 5; + private static final int HEIGHT = 1800; + private static final int WIDTH = 900; + private static final Binder BINDER = new Binder("binder"); + + private Context mContext; + private VirtualDeviceImpl mDeviceImpl; + private InputController mInputController; + @Mock + private InputController.NativeWrapper mNativeWrapperMock; + @Mock + private DisplayManagerInternal mDisplayManagerInternalMock; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + LocalServices.removeServiceForTest(DisplayManagerInternal.class); + LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); + + mContext = Mockito.spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); + doNothing().when(mContext).enforceCallingOrSelfPermission( + eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); + mInputController = new InputController(new Object(), mNativeWrapperMock); + mDeviceImpl = new VirtualDeviceImpl(mContext, + /* association info */ null, new Binder(), /* uid */ 0, mInputController, + (int associationId) -> {}); + } + + @Test + public void createVirtualKeyboard_noDisplay_failsSecurityException() { + assertThrows( + SecurityException.class, + () -> mDeviceImpl.createVirtualKeyboard(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, + PRODUCT_ID, BINDER)); + } + + @Test + public void createVirtualMouse_noDisplay_failsSecurityException() { + assertThrows( + SecurityException.class, + () -> mDeviceImpl.createVirtualMouse(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, + PRODUCT_ID, BINDER)); + } + + @Test + public void createVirtualTouchscreen_noDisplay_failsSecurityException() { + assertThrows( + SecurityException.class, + () -> mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME, + VENDOR_ID, PRODUCT_ID, BINDER, new Point(WIDTH, HEIGHT))); + } + + @Test + public void createVirtualKeyboard_noPermission_failsSecurityException() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); + doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( + eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); + assertThrows( + SecurityException.class, + () -> mDeviceImpl.createVirtualKeyboard(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, + PRODUCT_ID, BINDER)); + } + + @Test + public void createVirtualMouse_noPermission_failsSecurityException() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); + doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( + eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); + assertThrows( + SecurityException.class, + () -> mDeviceImpl.createVirtualMouse(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, + PRODUCT_ID, BINDER)); + } + + @Test + public void createVirtualTouchscreen_noPermission_failsSecurityException() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); + doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( + eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); + assertThrows( + SecurityException.class, + () -> mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME, + VENDOR_ID, PRODUCT_ID, BINDER, new Point(WIDTH, HEIGHT))); + } + + @Test + public void createVirtualKeyboard_hasDisplay_obtainFileDescriptor() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); + mDeviceImpl.createVirtualKeyboard(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, PRODUCT_ID, + BINDER); + assertWithMessage("Virtual keyboard should register fd when the display matches") + .that(mInputController.mInputDeviceFds).isNotEmpty(); + verify(mNativeWrapperMock).openUinputKeyboard(DEVICE_NAME, VENDOR_ID, PRODUCT_ID); + } + + @Test + public void createVirtualMouse_hasDisplay_obtainFileDescriptor() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); + mDeviceImpl.createVirtualMouse(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, PRODUCT_ID, + BINDER); + assertWithMessage("Virtual keyboard should register fd when the display matches") + .that(mInputController.mInputDeviceFds).isNotEmpty(); + verify(mNativeWrapperMock).openUinputMouse(DEVICE_NAME, VENDOR_ID, PRODUCT_ID); + } + + @Test + public void createVirtualTouchscreen_hasDisplay_obtainFileDescriptor() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); + mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, PRODUCT_ID, + BINDER, new Point(WIDTH, HEIGHT)); + assertWithMessage("Virtual keyboard should register fd when the display matches") + .that(mInputController.mInputDeviceFds).isNotEmpty(); + verify(mNativeWrapperMock).openUinputTouchscreen(DEVICE_NAME, VENDOR_ID, PRODUCT_ID, HEIGHT, + WIDTH); + } + + @Test + public void sendKeyEvent_noFd() { + assertThrows( + IllegalArgumentException.class, + () -> + mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder() + .setKeyCode(KeyEvent.KEYCODE_A) + .setAction(VirtualKeyEvent.ACTION_DOWN).build())); + } + + @Test + public void sendKeyEvent_hasFd_writesEvent() { + final int fd = 1; + final int keyCode = KeyEvent.KEYCODE_A; + final int action = VirtualKeyEvent.ACTION_UP; + mInputController.mInputDeviceFds.put(BINDER, fd); + mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder().setKeyCode(keyCode) + .setAction(action).build()); + verify(mNativeWrapperMock).writeKeyEvent(fd, keyCode, action); + } + + @Test + public void sendButtonEvent_noFd() { + assertThrows( + IllegalArgumentException.class, + () -> + mDeviceImpl.sendButtonEvent(BINDER, + new VirtualMouseButtonEvent.Builder() + .setButtonCode(VirtualMouseButtonEvent.BUTTON_BACK) + .setAction(VirtualMouseButtonEvent.ACTION_BUTTON_PRESS) + .build())); + } + + @Test + public void sendButtonEvent_hasFd_writesEvent() { + final int fd = 1; + final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK; + final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS; + mInputController.mInputDeviceFds.put(BINDER, fd); + mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder() + .setButtonCode(buttonCode) + .setAction(action).build()); + verify(mNativeWrapperMock).writeButtonEvent(fd, buttonCode, action); + } + + @Test + public void sendRelativeEvent_noFd() { + assertThrows( + IllegalArgumentException.class, + () -> + mDeviceImpl.sendRelativeEvent(BINDER, + new VirtualMouseRelativeEvent.Builder().setRelativeX( + 0.0f).setRelativeY(0.0f).build())); + } + + @Test + public void sendRelativeEvent_hasFd_writesEvent() { + final int fd = 1; + final float x = -0.2f; + final float y = 0.7f; + mInputController.mInputDeviceFds.put(BINDER, fd); + mDeviceImpl.sendRelativeEvent(BINDER, new VirtualMouseRelativeEvent.Builder() + .setRelativeX(x).setRelativeY(y).build()); + verify(mNativeWrapperMock).writeRelativeEvent(fd, x, y); + } + + @Test + public void sendScrollEvent_noFd() { + assertThrows( + IllegalArgumentException.class, + () -> + mDeviceImpl.sendScrollEvent(BINDER, + new VirtualMouseScrollEvent.Builder() + .setXAxisMovement(-1f) + .setYAxisMovement(1f).build())); + } + + @Test + public void sendScrollEvent_hasFd_writesEvent() { + final int fd = 1; + final float x = 0.5f; + final float y = 1f; + mInputController.mInputDeviceFds.put(BINDER, fd); + mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder() + .setXAxisMovement(x) + .setYAxisMovement(y).build()); + verify(mNativeWrapperMock).writeScrollEvent(fd, x, y); + } + + @Test + public void sendTouchEvent_noFd() { + assertThrows( + IllegalArgumentException.class, + () -> + mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder() + .setX(0.0f) + .setY(0.0f) + .setAction(VirtualTouchEvent.ACTION_UP) + .setPointerId(1) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .build())); + } + + @Test + public void sendTouchEvent_hasFd_writesEvent_withoutPressureOrMajorAxisSize() { + final int fd = 1; + final int pointerId = 5; + final int toolType = VirtualTouchEvent.TOOL_TYPE_FINGER; + final float x = 100.5f; + final float y = 200.5f; + final int action = VirtualTouchEvent.ACTION_UP; + mInputController.mInputDeviceFds.put(BINDER, fd); + mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x) + .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType).build()); + verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, Float.NaN, + Float.NaN); + } + + @Test + public void sendTouchEvent_hasFd_writesEvent() { + final int fd = 1; + final int pointerId = 5; + final int toolType = VirtualTouchEvent.TOOL_TYPE_FINGER; + final float x = 100.5f; + final float y = 200.5f; + final int action = VirtualTouchEvent.ACTION_UP; + final float pressure = 1.0f; + final float majorAxisSize = 10.0f; + mInputController.mInputDeviceFds.put(BINDER, fd); + mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x) + .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType) + .setPressure(pressure).setMajorAxisSize(majorAxisSize).build()); + verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, pressure, + majorAxisSize); + } +} -- cgit v1.2.3-59-g8ed1b