diff options
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() |