summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/system-current.txt19
-rw-r--r--core/api/test-current.txt1
-rw-r--r--core/java/android/companion/virtual/IVirtualDevice.aidl5
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceManager.java26
-rw-r--r--core/java/android/hardware/input/VirtualNavigationTouchpad.java60
-rw-r--r--core/java/android/hardware/input/VirtualNavigationTouchpadConfig.aidl19
-rw-r--r--core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java115
-rw-r--r--core/java/android/view/MotionEvent.java1
-rw-r--r--services/companion/java/com/android/server/companion/virtual/InputController.java34
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java33
-rw-r--r--services/core/java/com/android/server/input/InputManagerInternal.java15
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java53
-rw-r--r--services/core/java/com/android/server/input/NativeInputManagerService.java5
-rw-r--r--services/core/jni/com_android_server_input_InputManagerService.cpp53
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java35
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java95
-rw-r--r--services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt24
17 files changed, 575 insertions, 18 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index df8a47dcdfdd..566ac4535b0d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3000,6 +3000,7 @@ package android.companion.virtual {
method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.input.VirtualMouseConfig);
method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+ method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig);
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig);
method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
method public int getDeviceId();
@@ -4784,6 +4785,24 @@ package android.hardware.input {
method @NonNull public android.hardware.input.VirtualMouseScrollEvent.Builder setYAxisMovement(@FloatRange(from=-1.0F, to=1.0f) float);
}
+ public class VirtualNavigationTouchpad 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);
+ }
+
+ public final class VirtualNavigationTouchpadConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getHeight();
+ method public int getWidth();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualNavigationTouchpadConfig> CREATOR;
+ }
+
+ public static final class VirtualNavigationTouchpadConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualNavigationTouchpadConfig.Builder> {
+ ctor public VirtualNavigationTouchpadConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int);
+ method @NonNull public android.hardware.input.VirtualNavigationTouchpadConfig build();
+ }
+
public final class VirtualTouchEvent implements android.os.Parcelable {
method public int describeContents();
method public int getAction();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 9730c169243c..5e02e72f2088 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2944,6 +2944,7 @@ package android.view {
}
public final class MotionEvent extends android.view.InputEvent implements android.os.Parcelable {
+ method public int getDisplayId();
method public void setActionButton(int);
method public void setButtonState(int);
method public void setDisplayId(int);
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 5c47ea2aa3f0..f17d18652e98 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -33,6 +33,7 @@ import android.hardware.input.VirtualMouseRelativeEvent;
import android.hardware.input.VirtualMouseScrollEvent;
import android.hardware.input.VirtualTouchEvent;
import android.hardware.input.VirtualTouchscreenConfig;
+import android.hardware.input.VirtualNavigationTouchpadConfig;
import android.os.ResultReceiver;
/**
@@ -84,6 +85,10 @@ interface IVirtualDevice {
void createVirtualTouchscreen(
in VirtualTouchscreenConfig config,
IBinder token);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ void createVirtualNavigationTouchpad(
+ in VirtualNavigationTouchpadConfig config,
+ IBinder token);
void unregisterInputDevice(IBinder token);
int getInputDeviceId(IBinder token);
boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index e8180a367f61..dba7c8e630c8 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -49,6 +49,8 @@ import android.hardware.input.VirtualKeyboard;
import android.hardware.input.VirtualKeyboardConfig;
import android.hardware.input.VirtualMouse;
import android.hardware.input.VirtualMouseConfig;
+import android.hardware.input.VirtualNavigationTouchpad;
+import android.hardware.input.VirtualNavigationTouchpadConfig;
import android.hardware.input.VirtualTouchscreen;
import android.hardware.input.VirtualTouchscreenConfig;
import android.os.Binder;
@@ -660,6 +662,30 @@ public final class VirtualDeviceManager {
}
/**
+ * Creates a virtual touchpad in navigation mode.
+ *
+ * A touchpad in navigation mode means that its events are interpreted as navigation events
+ * (up, down, etc) instead of using them to update a cursor's absolute position. If the
+ * events are not consumed they are converted to DPAD events.
+ *
+ * @param config the configurations of the virtual navigation touchpad.
+ */
+ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ @NonNull
+ public VirtualNavigationTouchpad createVirtualNavigationTouchpad(
+ @NonNull VirtualNavigationTouchpadConfig config) {
+ try {
+ final IBinder token = new Binder(
+ "android.hardware.input.VirtualNavigationTouchpad:"
+ + config.getInputDeviceName());
+ mVirtualDevice.createVirtualNavigationTouchpad(config, token);
+ return new VirtualNavigationTouchpad(mVirtualDevice, token);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Creates a virtual touchscreen.
*
* @param display the display that the events inputted through this device should
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpad.java b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
new file mode 100644
index 000000000000..2854034cd127
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 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;
+
+/**
+ * A virtual navigation touchpad representing a touch-based input mechanism on a remote device.
+ *
+ * <p>This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.
+ *
+ * <p>The virtual touchpad will be in navigation mode. Motion results in focus traversal in the same
+ * manner as D-Pad navigation if the events are not consumed.
+ *
+ * @see android.view.InputDevice#SOURCE_TOUCH_NAVIGATION
+ *
+ * @hide
+ */
+@SystemApi
+public class VirtualNavigationTouchpad extends VirtualInputDevice {
+
+ /** @hide */
+ public VirtualNavigationTouchpad(IVirtualDevice virtualDevice, IBinder token) {
+ super(virtualDevice, token);
+ }
+
+ /**
+ * 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/hardware/input/VirtualNavigationTouchpadConfig.aidl b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.aidl
new file mode 100644
index 000000000000..d9124910adff
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 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 VirtualNavigationTouchpadConfig;
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
new file mode 100644
index 000000000000..f2805bb1029e
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2022 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.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Configurations to create virtual navigation touchpad.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualNavigationTouchpadConfig extends VirtualInputDeviceConfig
+ implements Parcelable {
+
+ /** The touchpad height. */
+ private final int mHeight;
+ /** The touchpad width. */
+ private final int mWidth;
+
+ private VirtualNavigationTouchpadConfig(@NonNull Builder builder) {
+ super(builder);
+ mHeight = builder.mHeight;
+ mWidth = builder.mWidth;
+ }
+
+ private VirtualNavigationTouchpadConfig(@NonNull Parcel in) {
+ super(in);
+ mHeight = in.readInt();
+ mWidth = in.readInt();
+ }
+
+ /** Returns the touchpad height. */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /** Returns the touchpad width. */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mHeight);
+ dest.writeInt(mWidth);
+ }
+
+ @NonNull
+ public static final Creator<VirtualNavigationTouchpadConfig> CREATOR =
+ new Creator<VirtualNavigationTouchpadConfig>() {
+ @Override
+ public VirtualNavigationTouchpadConfig createFromParcel(Parcel in) {
+ return new VirtualNavigationTouchpadConfig(in);
+ }
+
+ @Override
+ public VirtualNavigationTouchpadConfig[] newArray(int size) {
+ return new VirtualNavigationTouchpadConfig[size];
+ }
+ };
+
+ /**
+ * Builder for creating a {@link VirtualNavigationTouchpadConfig}.
+ */
+ public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
+
+ private final int mHeight;
+ private final int mWidth;
+
+ public Builder(@IntRange(from = 1) int touchpadHeight,
+ @IntRange(from = 1) int touchpadWidth) {
+ if (touchpadHeight <= 0 || touchpadWidth <= 0) {
+ throw new IllegalArgumentException(
+ "Cannot create a virtual navigation touchpad, touchpad dimensions must be "
+ + "positive. Got: (" + touchpadHeight + ", "
+ + touchpadWidth + ")");
+ }
+ mHeight = touchpadHeight;
+ mWidth = touchpadWidth;
+ }
+
+ /**
+ * Builds the {@link VirtualNavigationTouchpadConfig} instance.
+ */
+ @NonNull
+ public VirtualNavigationTouchpadConfig build() {
+ return new VirtualNavigationTouchpadConfig(this);
+ }
+ }
+}
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index c8a5d8d887f9..4fbb249c507f 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -2199,6 +2199,7 @@ public final class MotionEvent extends InputEvent implements Parcelable {
}
/** @hide */
+ @TestApi
@Override
public int getDisplayId() {
return nativeGetDisplayId(mNativePtr);
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 0cea3d0575c6..97b5d6ddf6b6 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -61,15 +61,19 @@ class InputController {
private static final AtomicLong sNextPhysId = new AtomicLong(1);
+ static final String NAVIGATION_TOUCHPAD_DEVICE_TYPE = "touchNavigation";
+
static final String PHYS_TYPE_DPAD = "Dpad";
static final String PHYS_TYPE_KEYBOARD = "Keyboard";
static final String PHYS_TYPE_MOUSE = "Mouse";
static final String PHYS_TYPE_TOUCHSCREEN = "Touchscreen";
+ static final String PHYS_TYPE_NAVIGATION_TOUCHPAD = "NavigationTouchpad";
@StringDef(prefix = { "PHYS_TYPE_" }, value = {
PHYS_TYPE_DPAD,
PHYS_TYPE_KEYBOARD,
PHYS_TYPE_MOUSE,
PHYS_TYPE_TOUCHSCREEN,
+ PHYS_TYPE_NAVIGATION_TOUCHPAD,
})
@Retention(RetentionPolicy.SOURCE)
@interface PhysType {
@@ -190,6 +194,28 @@ class InputController {
}
}
+ void createNavigationTouchpad(
+ @NonNull String deviceName,
+ int vendorId,
+ int productId,
+ @NonNull IBinder deviceToken,
+ int displayId,
+ int touchpadHeight,
+ int touchpadWidth) {
+ final String phys = createPhys(PHYS_TYPE_NAVIGATION_TOUCHPAD);
+ mInputManagerInternal.setTypeAssociation(phys, NAVIGATION_TOUCHPAD_DEVICE_TYPE);
+ try {
+ createDeviceInternal(InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD, deviceName,
+ vendorId, productId, deviceToken, displayId, phys,
+ () -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId,
+ phys, touchpadHeight, touchpadWidth));
+ } catch (DeviceCreationException e) {
+ mInputManagerInternal.unsetTypeAssociation(phys);
+ throw new RuntimeException(
+ "Failed to create virtual navigation touchpad device '" + deviceName + "'.", e);
+ }
+ }
+
void unregisterInputDevice(@NonNull IBinder token) {
synchronized (mLock) {
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.remove(
@@ -207,7 +233,13 @@ class InputController {
InputDeviceDescriptor inputDeviceDescriptor) {
token.unlinkToDeath(inputDeviceDescriptor.getDeathRecipient(), /* flags= */ 0);
mNativeWrapper.closeUinput(inputDeviceDescriptor.getFileDescriptor());
+
InputManager.getInstance().removeUniqueIdAssociation(inputDeviceDescriptor.getPhys());
+ // Type associations are added in the case of navigation touchpads. Those should be removed
+ // once the input device gets closed.
+ if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD) {
+ mInputManagerInternal.unsetTypeAssociation(inputDeviceDescriptor.getPhys());
+ }
// Reset values to the default if all virtual mice are unregistered, or set display
// id if there's another mouse (choose the most recent). The inputDeviceDescriptor must be
@@ -509,11 +541,13 @@ class InputController {
static final int TYPE_MOUSE = 2;
static final int TYPE_TOUCHSCREEN = 3;
static final int TYPE_DPAD = 4;
+ static final int TYPE_NAVIGATION_TOUCHPAD = 5;
@IntDef(prefix = { "TYPE_" }, value = {
TYPE_KEYBOARD,
TYPE_MOUSE,
TYPE_TOUCHSCREEN,
TYPE_DPAD,
+ TYPE_NAVIGATION_TOUCHPAD,
})
@Retention(RetentionPolicy.SOURCE)
@interface Type {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 2f5c40a693a6..12ad9f1cf580 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -53,6 +53,7 @@ import android.hardware.input.VirtualMouseButtonEvent;
import android.hardware.input.VirtualMouseConfig;
import android.hardware.input.VirtualMouseRelativeEvent;
import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualNavigationTouchpadConfig;
import android.hardware.input.VirtualTouchEvent;
import android.hardware.input.VirtualTouchscreenConfig;
import android.os.Binder;
@@ -491,6 +492,38 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
}
@Override // Binder call
+ public void createVirtualNavigationTouchpad(VirtualNavigationTouchpadConfig config,
+ @NonNull IBinder deviceToken) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+ "Permission required to create a virtual navigation touchpad");
+ synchronized (mVirtualDeviceLock) {
+ if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
+ throw new SecurityException(
+ "Cannot create a virtual navigation touchpad for a display not associated "
+ + "with this virtual device");
+ }
+ }
+ int touchpadHeight = config.getHeight();
+ int touchpadWidth = config.getWidth();
+ if (touchpadHeight <= 0 || touchpadWidth <= 0) {
+ throw new IllegalArgumentException(
+ "Cannot create a virtual navigation touchpad, touchpad dimensions must be positive."
+ + " Got: (" + touchpadHeight + ", " + touchpadWidth + ")");
+ }
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mInputController.createNavigationTouchpad(
+ config.getInputDeviceName(), config.getVendorId(),
+ config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
+ touchpadHeight, touchpadWidth);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override // Binder call
public void unregisterInputDevice(IBinder token) {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 298098a572b2..01a564d6816f 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -171,4 +171,19 @@ public abstract class InputManagerInternal {
* {@see Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT}
*/
public abstract void decrementKeyboardBacklight(int deviceId);
+
+ /**
+ * Add a runtime association between the input port and device type. Input ports are expected to
+ * be unique.
+ * @param inputPort The port of the input device.
+ * @param type The type of the device. E.g. "touchNavigation".
+ */
+ public abstract void setTypeAssociation(@NonNull String inputPort, @NonNull String type);
+
+ /**
+ * Removes a runtime association between the input device and type.
+ *
+ * @param inputPort The port of the input device.
+ */
+ public abstract void unsetTypeAssociation(@NonNull String inputPort);
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index c62abf004daa..1809b1821b5e 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -248,6 +248,13 @@ public class InputManagerService extends IInputManager.Stub
@GuardedBy("mAssociationsLock")
private final Map<String, String> mUniqueIdAssociations = new ArrayMap<>();
+ // Stores input ports associated with device types. For example, adding an association
+ // {"123", "touchNavigation"} here would mean that a touch device appearing at port "123" would
+ // enumerate as a "touch navigation" device rather than the default "touchpad as a mouse
+ // pointer" device.
+ @GuardedBy("mAssociationsLock")
+ private final Map<String, String> mDeviceTypeAssociations = new ArrayMap<>();
+
// Guards per-display input properties and properties relating to the mouse pointer.
// Threads can wait on this lock to be notified the next time the display on which the mouse
// pointer is shown has changed.
@@ -1905,6 +1912,23 @@ public class InputManagerService extends IInputManager.Stub
mNative.changeUniqueIdAssociation();
}
+ void setTypeAssociationInternal(@NonNull String inputPort, @NonNull String type) {
+ Objects.requireNonNull(inputPort);
+ Objects.requireNonNull(type);
+ synchronized (mAssociationsLock) {
+ mDeviceTypeAssociations.put(inputPort, type);
+ }
+ mNative.changeTypeAssociation();
+ }
+
+ void unsetTypeAssociationInternal(@NonNull String inputPort) {
+ Objects.requireNonNull(inputPort);
+ synchronized (mAssociationsLock) {
+ mDeviceTypeAssociations.remove(inputPort);
+ }
+ mNative.changeTypeAssociation();
+ }
+
@Override // Binder call
public InputSensorInfo[] getSensorList(int deviceId) {
return mNative.getSensorList(deviceId);
@@ -2221,6 +2245,13 @@ public class InputManagerService extends IInputManager.Stub
pw.println(" uniqueId: " + v);
});
}
+ if (!mDeviceTypeAssociations.isEmpty()) {
+ pw.println("Type Associations:");
+ mDeviceTypeAssociations.forEach((k, v) -> {
+ pw.print(" port: " + k);
+ pw.println(" type: " + v);
+ });
+ }
}
}
@@ -2630,6 +2661,18 @@ public class InputManagerService extends IInputManager.Stub
return flatten(associations);
}
+ // Native callback
+ @SuppressWarnings("unused")
+ @VisibleForTesting
+ String[] getDeviceTypeAssociations() {
+ final Map<String, String> associations;
+ synchronized (mAssociationsLock) {
+ associations = new HashMap<>(mDeviceTypeAssociations);
+ }
+
+ return flatten(associations);
+ }
+
/**
* Gets if an input device could dispatch to the given display".
* @param deviceId The input device id.
@@ -3263,6 +3306,16 @@ public class InputManagerService extends IInputManager.Stub
public void decrementKeyboardBacklight(int deviceId) {
mKeyboardBacklightController.decrementKeyboardBacklight(deviceId);
}
+
+ @Override
+ public void setTypeAssociation(@NonNull String inputPort, @NonNull String type) {
+ setTypeAssociationInternal(inputPort, type);
+ }
+
+ @Override
+ public void unsetTypeAssociation(@NonNull String inputPort) {
+ unsetTypeAssociationInternal(inputPort);
+ }
}
@Override
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 8781c6e2b934..184bc0e3519d 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -186,6 +186,8 @@ interface NativeInputManagerService {
void changeUniqueIdAssociation();
+ void changeTypeAssociation();
+
void notifyPointerDisplayIdChanged();
void setDisplayEligibilityForPointerCapture(int displayId, boolean enabled);
@@ -400,6 +402,9 @@ interface NativeInputManagerService {
public native void changeUniqueIdAssociation();
@Override
+ public native void changeTypeAssociation();
+
+ @Override
public native void notifyPointerDisplayIdChanged();
@Override
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 145e0885b105..c36c57159279 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -120,6 +120,7 @@ static struct {
jmethodID getExcludedDeviceNames;
jmethodID getInputPortAssociations;
jmethodID getInputUniqueIdAssociations;
+ jmethodID getDeviceTypeAssociations;
jmethodID getKeyRepeatTimeout;
jmethodID getKeyRepeatDelay;
jmethodID getHoverTapTimeout;
@@ -411,6 +412,8 @@ private:
void ensureSpriteControllerLocked();
sp<SurfaceControl> getParentSurfaceForPointers(int displayId);
static bool checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName);
+ std::unordered_map<std::string, std::string> readMapFromInterleavedJavaArray(
+ jmethodID method, const char* methodName);
static inline JNIEnv* jniEnv() { return AndroidRuntime::getJNIEnv(); }
};
@@ -583,21 +586,14 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon
}
env->DeleteLocalRef(portAssociations);
}
- outConfig->uniqueIdAssociations.clear();
- jobjectArray uniqueIdAssociations = jobjectArray(
- env->CallObjectMethod(mServiceObj, gServiceClassInfo.getInputUniqueIdAssociations));
- if (!checkAndClearExceptionFromCallback(env, "getInputUniqueIdAssociations") &&
- uniqueIdAssociations) {
- jsize length = env->GetArrayLength(uniqueIdAssociations);
- for (jsize i = 0; i < length / 2; i++) {
- std::string inputDeviceUniqueId =
- getStringElementFromJavaArray(env, uniqueIdAssociations, 2 * i);
- std::string displayUniqueId =
- getStringElementFromJavaArray(env, uniqueIdAssociations, 2 * i + 1);
- outConfig->uniqueIdAssociations.insert({inputDeviceUniqueId, displayUniqueId});
- }
- env->DeleteLocalRef(uniqueIdAssociations);
- }
+
+ outConfig->uniqueIdAssociations =
+ readMapFromInterleavedJavaArray(gServiceClassInfo.getInputUniqueIdAssociations,
+ "getInputUniqueIdAssociations");
+
+ outConfig->deviceTypeAssociations =
+ readMapFromInterleavedJavaArray(gServiceClassInfo.getDeviceTypeAssociations,
+ "getDeviceTypeAssociations");
jint hoverTapTimeout = env->CallIntMethod(mServiceObj,
gServiceClassInfo.getHoverTapTimeout);
@@ -647,6 +643,23 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon
} // release lock
}
+std::unordered_map<std::string, std::string> NativeInputManager::readMapFromInterleavedJavaArray(
+ jmethodID method, const char* methodName) {
+ JNIEnv* env = jniEnv();
+ jobjectArray javaArray = jobjectArray(env->CallObjectMethod(mServiceObj, method));
+ std::unordered_map<std::string, std::string> map;
+ if (!checkAndClearExceptionFromCallback(env, methodName) && javaArray) {
+ jsize length = env->GetArrayLength(javaArray);
+ for (jsize i = 0; i < length / 2; i++) {
+ std::string key = getStringElementFromJavaArray(env, javaArray, 2 * i);
+ std::string value = getStringElementFromJavaArray(env, javaArray, 2 * i + 1);
+ map.insert({key, value});
+ }
+ }
+ env->DeleteLocalRef(javaArray);
+ return map;
+}
+
std::shared_ptr<PointerControllerInterface> NativeInputManager::obtainPointerController(
int32_t /* deviceId */) {
ATRACE_CALL();
@@ -2237,6 +2250,12 @@ static void nativeChangeUniqueIdAssociation(JNIEnv* env, jobject nativeImplObj)
InputReaderConfiguration::CHANGE_DISPLAY_INFO);
}
+static void nativeChangeTypeAssociation(JNIEnv* env, jobject nativeImplObj) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+ im->getInputManager()->getReader().requestRefreshConfiguration(
+ InputReaderConfiguration::CHANGE_DEVICE_TYPE);
+}
+
static void nativeSetMotionClassifierEnabled(JNIEnv* env, jobject nativeImplObj, jboolean enabled) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2425,6 +2444,7 @@ static const JNINativeMethod gInputManagerMethods[] = {
{"canDispatchToDisplay", "(II)Z", (void*)nativeCanDispatchToDisplay},
{"notifyPortAssociationsChanged", "()V", (void*)nativeNotifyPortAssociationsChanged},
{"changeUniqueIdAssociation", "()V", (void*)nativeChangeUniqueIdAssociation},
+ {"changeTypeAssociation", "()V", (void*)nativeChangeTypeAssociation},
{"setDisplayEligibilityForPointerCapture", "(IZ)V",
(void*)nativeSetDisplayEligibilityForPointerCapture},
{"setMotionClassifierEnabled", "(Z)V", (void*)nativeSetMotionClassifierEnabled},
@@ -2546,6 +2566,9 @@ int register_android_server_InputManager(JNIEnv* env) {
GET_METHOD_ID(gServiceClassInfo.getInputUniqueIdAssociations, clazz,
"getInputUniqueIdAssociations", "()[Ljava/lang/String;");
+ GET_METHOD_ID(gServiceClassInfo.getDeviceTypeAssociations, clazz, "getDeviceTypeAssociations",
+ "()[Ljava/lang/String;");
+
GET_METHOD_ID(gServiceClassInfo.getKeyRepeatTimeout, clazz,
"getKeyRepeatTimeout", "()I");
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index d2f2af1b91b6..9c7c574f8e31 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -143,4 +144,38 @@ public class InputControllerTest {
verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
}
+ @Test
+ public void createNavigationTouchpad_hasDeviceId() {
+ final IBinder deviceToken = new Binder();
+ mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1,
+ deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50);
+
+ int deviceId = mInputController.getInputDeviceId(deviceToken);
+ int[] deviceIds = InputManager.getInstance().getInputDeviceIds();
+
+ assertWithMessage("InputManager's deviceIds list should contain id of the device").that(
+ deviceIds).asList().contains(deviceId);
+ }
+
+ @Test
+ public void createNavigationTouchpad_setsTypeAssociation() {
+ final IBinder deviceToken = new Binder();
+ mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1,
+ deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50);
+
+ verify(mInputManagerInternalMock).setTypeAssociation(
+ startsWith("virtualNavigationTouchpad:"), eq("touchNavigation"));
+ }
+
+ @Test
+ public void createAndUnregisterNavigationTouchpad_unsetsTypeAssociation() {
+ final IBinder deviceToken = new Binder();
+ mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1,
+ deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50);
+
+ mInputController.unregisterInputDevice(deviceToken);
+
+ verify(mInputManagerInternalMock).unsetTypeAssociation(
+ startsWith("virtualNavigationTouchpad:"));
+ }
}
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
index 2d16409d74c1..31e53d56f520 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -69,6 +69,7 @@ import android.hardware.input.VirtualMouseButtonEvent;
import android.hardware.input.VirtualMouseConfig;
import android.hardware.input.VirtualMouseRelativeEvent;
import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualNavigationTouchpadConfig;
import android.hardware.input.VirtualTouchEvent;
import android.hardware.input.VirtualTouchscreenConfig;
import android.net.MacAddress;
@@ -175,6 +176,14 @@ public class VirtualDeviceManagerServiceTest {
.setWidthInPixels(WIDTH)
.setHeightInPixels(HEIGHT)
.build();
+ private static final VirtualNavigationTouchpadConfig NAVIGATION_TOUCHPAD_CONFIG =
+ new VirtualNavigationTouchpadConfig.Builder(
+ /* touchpadHeight= */ HEIGHT, /* touchpadWidth= */ WIDTH)
+ .setVendorId(VENDOR_ID)
+ .setProductId(PRODUCT_ID)
+ .setInputDeviceName(DEVICE_NAME)
+ .setAssociatedDisplayId(DISPLAY_ID)
+ .build();
private Context mContext;
private InputManagerMockHelper mInputManagerMockHelper;
@@ -700,8 +709,67 @@ public class VirtualDeviceManagerServiceTest {
.build();
mDeviceImpl.createVirtualTouchscreen(positiveConfig, BINDER);
assertWithMessage(
- "Virtual touchscreen should create input device descriptor on successful creation"
- + ".").that(mInputController.getInputDeviceDescriptors()).isNotEmpty();
+ "Virtual touchscreen should create input device descriptor on successful creation"
+ + ".").that(mInputController.getInputDeviceDescriptors()).isNotEmpty();
+ }
+
+ @Test
+ public void createVirtualNavigationTouchpad_noDisplay_failsSecurityException() {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
+ BINDER));
+ }
+
+ @Test
+ public void createVirtualNavigationTouchpad_zeroDisplayDimension_failsWithException() {
+ mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+ assertThrows(IllegalArgumentException.class,
+ () -> {
+ final VirtualNavigationTouchpadConfig zeroConfig =
+ new VirtualNavigationTouchpadConfig.Builder(
+ /* touchpadHeight= */ 0, /* touchpadWidth= */ 0)
+ .setVendorId(VENDOR_ID)
+ .setProductId(PRODUCT_ID)
+ .setInputDeviceName(DEVICE_NAME)
+ .setAssociatedDisplayId(DISPLAY_ID)
+ .build();
+ mDeviceImpl.createVirtualNavigationTouchpad(zeroConfig, BINDER);
+ });
+ }
+
+ @Test
+ public void createVirtualNavigationTouchpad_negativeDisplayDimension_failsWithException() {
+ mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+ assertThrows(IllegalArgumentException.class,
+ () -> {
+ final VirtualNavigationTouchpadConfig zeroConfig =
+ new VirtualNavigationTouchpadConfig.Builder(
+ /* touchpadHeight= */ -50, /* touchpadWidth= */ 50)
+ .setVendorId(VENDOR_ID)
+ .setProductId(PRODUCT_ID)
+ .setInputDeviceName(DEVICE_NAME)
+ .setAssociatedDisplayId(DISPLAY_ID)
+ .build();
+ mDeviceImpl.createVirtualNavigationTouchpad(zeroConfig, BINDER);
+ });
+ }
+
+ @Test
+ public void createVirtualNavigationTouchpad_positiveDisplayDimension_successful() {
+ mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+ VirtualNavigationTouchpadConfig positiveConfig =
+ new VirtualNavigationTouchpadConfig.Builder(
+ /* touchpadHeight= */ 50, /* touchpadWidth= */ 50)
+ .setVendorId(VENDOR_ID)
+ .setProductId(PRODUCT_ID)
+ .setInputDeviceName(DEVICE_NAME)
+ .setAssociatedDisplayId(DISPLAY_ID)
+ .build();
+ mDeviceImpl.createVirtualNavigationTouchpad(positiveConfig, BINDER);
+ assertWithMessage(
+ "Virtual navigation touchpad should create input device descriptor on successful "
+ + "creation"
+ + ".").that(mInputController.getInputDeviceDescriptors()).isNotEmpty();
}
@Test
@@ -748,6 +816,16 @@ public class VirtualDeviceManagerServiceTest {
}
@Test
+ public void createVirtualNavigationTouchpad_noPermission_failsSecurityException() {
+ mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+ doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
+ eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
+ BINDER));
+ }
+
+ @Test
public void createVirtualSensor_noPermission_failsSecurityException() {
doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
@@ -811,7 +889,18 @@ public class VirtualDeviceManagerServiceTest {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER);
assertWithMessage("Virtual touchscreen should register fd when the display matches").that(
- mInputController.getInputDeviceDescriptors()).isNotEmpty();
+ mInputController.getInputDeviceDescriptors()).isNotEmpty();
+ verify(mNativeWrapperMock).openUinputTouchscreen(eq(DEVICE_NAME), eq(VENDOR_ID),
+ eq(PRODUCT_ID), anyString(), eq(HEIGHT), eq(WIDTH));
+ }
+
+ @Test
+ public void createVirtualNavigationTouchpad_hasDisplay_obtainFileDescriptor() {
+ mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+ mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, BINDER);
+ assertWithMessage("Virtual navigation touchpad should register fd when the display matches")
+ .that(
+ mInputController.getInputDeviceDescriptors()).isNotEmpty();
verify(mNativeWrapperMock).openUinputTouchscreen(eq(DEVICE_NAME), eq(VENDOR_ID),
eq(PRODUCT_ID), anyString(), eq(HEIGHT), eq(WIDTH));
}
diff --git a/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt
index e390bccf41d8..3326f80f5f91 100644
--- a/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt
@@ -16,6 +16,7 @@
package com.android.server.input
+
import android.content.Context
import android.content.ContextWrapper
import android.hardware.display.DisplayViewport
@@ -25,6 +26,7 @@ import android.platform.test.annotations.Presubmit
import android.view.Display
import android.view.PointerIcon
import androidx.test.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.junit.Assert.assertFalse
@@ -287,6 +289,28 @@ class InputManagerServiceTests {
verify(native).setPointerAcceleration(eq(5f))
}
+ @Test
+ fun setDeviceTypeAssociation_setsDeviceTypeAssociation() {
+ val inputPort = "inputPort"
+ val type = "type"
+
+ localService.setTypeAssociation(inputPort, type)
+
+ assertThat(service.getDeviceTypeAssociations()).asList().containsExactly(inputPort, type)
+ .inOrder()
+ }
+
+ @Test
+ fun setAndUnsetDeviceTypeAssociation_deviceTypeAssociationIsMissing() {
+ val inputPort = "inputPort"
+ val type = "type"
+
+ localService.setTypeAssociation(inputPort, type)
+ localService.unsetTypeAssociation(inputPort)
+
+ assertTrue(service.getDeviceTypeAssociations().isEmpty())
+ }
+
private fun setVirtualMousePointerDisplayIdAndVerify(overrideDisplayId: Int) {
val thread = Thread { localService.setVirtualMousePointerDisplayId(overrideDisplayId) }
thread.start()