summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Christine Franks <christyfranks@google.com> 2021-11-19 14:46:16 -0800
committer Christine Franks <christyfranks@google.com> 2021-12-10 10:17:31 -0800
commitc4cf03b162a3ce0fddee7d913aca45ff178b5242 (patch)
tree54d3c618338b54bed35baeda71f9e03e7843b347
parenta2cb2b1fd182a765164614023252bd68110ef2c4 (diff)
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
-rw-r--r--core/api/system-current.txt121
-rw-r--r--core/java/android/companion/virtual/IVirtualDevice.aidl32
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceManager.java91
-rw-r--r--core/java/android/hardware/input/VirtualKeyEvent.aidl19
-rw-r--r--core/java/android/hardware/input/VirtualKeyEvent.java152
-rw-r--r--core/java/android/hardware/input/VirtualKeyboard.java72
-rw-r--r--core/java/android/hardware/input/VirtualMouse.java102
-rw-r--r--core/java/android/hardware/input/VirtualMouseButtonEvent.aidl19
-rw-r--r--core/java/android/hardware/input/VirtualMouseButtonEvent.java180
-rw-r--r--core/java/android/hardware/input/VirtualMouseRelativeEvent.aidl19
-rw-r--r--core/java/android/hardware/input/VirtualMouseRelativeEvent.java119
-rw-r--r--core/java/android/hardware/input/VirtualMouseScrollEvent.aidl19
-rw-r--r--core/java/android/hardware/input/VirtualMouseScrollEvent.java129
-rw-r--r--core/java/android/hardware/input/VirtualTouchEvent.aidl19
-rw-r--r--core/java/android/hardware/input/VirtualTouchEvent.java299
-rw-r--r--core/java/android/hardware/input/VirtualTouchscreen.java71
-rw-r--r--core/java/android/view/MotionEvent.java9
-rw-r--r--core/tests/coretests/src/android/hardware/input/VirtualKeyEventTest.java60
-rw-r--r--core/tests/coretests/src/android/hardware/input/VirtualMouseButtonEventTest.java59
-rw-r--r--core/tests/coretests/src/android/hardware/input/VirtualMouseRelativeEventTest.java37
-rw-r--r--core/tests/coretests/src/android/hardware/input/VirtualMouseScrollEventTest.java54
-rw-r--r--core/tests/coretests/src/android/hardware/input/VirtualTouchEventTest.java172
-rw-r--r--services/companion/java/com/android/server/companion/virtual/InputController.java283
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java278
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java80
-rw-r--r--services/core/jni/Android.bp1
-rw-r--r--services/core/jni/com_android_server_companion_virtual_InputController.cpp455
-rw-r--r--services/core/jni/onload.cpp2
-rw-r--r--services/tests/servicestests/Android.bp1
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java313
30 files changed, 3203 insertions, 64 deletions
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<android.hardware.input.VirtualKeyEvent> 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<android.hardware.input.VirtualMouseButtonEvent> 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<android.hardware.input.VirtualMouseRelativeEvent> 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<android.hardware.input.VirtualMouseScrollEvent> 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<android.hardware.input.VirtualTouchEvent> 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<VirtualKeyEvent> CREATOR =
+ new Parcelable.Creator<VirtualKeyEvent>() {
+ 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<VirtualMouseButtonEvent> CREATOR =
+ new Parcelable.Creator<VirtualMouseButtonEvent>() {
+ 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<VirtualMouseRelativeEvent> CREATOR =
+ new Parcelable.Creator<VirtualMouseRelativeEvent>() {
+ 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<VirtualMouseScrollEvent> CREATOR =
+ new Parcelable.Creator<VirtualMouseScrollEvent>() {
+ 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<VirtualTouchEvent> CREATOR =
+ new Parcelable.Creator<VirtualTouchEvent>() {
+ 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<IBinder, Integer> 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<Integer> 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<Integer> 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 <android-base/unique_fd.h>
+#include <android/input.h>
+#include <android/keycodes.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/uinput.h>
+#include <math.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedUtfChars.h>
+#include <utils/Log.h>
+
+#include <map>
+#include <string>
+
+namespace android {
+
+enum class DeviceType {
+ KEYBOARD,
+ MOUSE,
+ TOUCHSCREEN,
+};
+
+enum class UinputAction {
+ RELEASE = 0,
+ PRESS = 1,
+ MOVE = 2,
+ CANCEL = 3,
+};
+
+static std::map<int, UinputAction> BUTTON_ACTION_MAPPING = {
+ {AMOTION_EVENT_ACTION_BUTTON_PRESS, UinputAction::PRESS},
+ {AMOTION_EVENT_ACTION_BUTTON_RELEASE, UinputAction::RELEASE},
+};
+
+static std::map<int, UinputAction> KEY_ACTION_MAPPING = {
+ {AKEY_EVENT_ACTION_DOWN, UinputAction::PRESS},
+ {AKEY_EVENT_ACTION_UP, UinputAction::RELEASE},
+};
+
+static std::map<int, UinputAction> 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<int, int> 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<int, int> 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<int, int> 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<uint16_t>(keyCodeIterator->second),
+ static_cast<int32_t>(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<uint16_t>(buttonCodeIterator->second),
+ static_cast<int32_t>(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<int32_t>(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<int32_t>(uinputAction))) {
+ return false;
+ }
+ if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID,
+ static_cast<int32_t>(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);
+ }
+}