From ca4f50c5ef12eee2c0efb562e157a8b2d75cb001 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:26:24 +0800 Subject: [PATCH] Associate UHID devices to virtual displays This allows the mouse pointer to appear on the correct display (only for devices running Android 15+). Fixes #5547 PR #6009 Signed-off-by: Romain Vimont --- .../genymobile/scrcpy/control/Controller.java | 29 ++++++++- .../scrcpy/control/UhidManager.java | 65 ++++++++++++++++--- .../genymobile/scrcpy/device/DisplayInfo.java | 9 ++- .../scrcpy/wrappers/DisplayManager.java | 5 +- .../scrcpy/wrappers/InputManager.java | 40 ++++++++++++ 5 files changed, 134 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 24d827fd..a905b6c9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; +import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Size; @@ -156,8 +157,34 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private UhidManager getUhidManager() { if (uhidManager == null) { - uhidManager = new UhidManager(sender); + int uhidDisplayId = displayId; + if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { + if (displayId == Device.DISPLAY_ID_NONE) { + // Mirroring a new virtual display id (using --new-display-id feature) on Android >= 15, where the UHID mouse pointer can be + // associated to the virtual display + try { + // Wait for at most 1 second until a virtual display id is known + DisplayData data = waitDisplayData(1000); + if (data != null) { + uhidDisplayId = data.virtualDisplayId; + } + } catch (InterruptedException e) { + // do nothing + } + } + } + + String displayUniqueId = null; + if (uhidDisplayId > 0) { + // Ignore Device.DISPLAY_ID_NONE and 0 (main display) + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(uhidDisplayId); + if (displayInfo != null) { + displayUniqueId = displayInfo.getUniqueId(); + } + } + uhidManager = new UhidManager(sender, displayUniqueId); } + return uhidManager; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index c4867a3f..20532c0b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.StringUtils; +import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.Build; import android.os.HandlerThread; @@ -31,14 +32,20 @@ public final class UhidManager { private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event) + // Must be unique across the system + private static final String INPUT_PORT = "scrcpy:" + Os.getpid(); + + private final String displayUniqueId; + private final ArrayMap fds = new ArrayMap<>(); private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final DeviceMessageSender sender; private final MessageQueue queue; - public UhidManager(DeviceMessageSender sender) { + public UhidManager(DeviceMessageSender sender, String displayUniqueId) { this.sender = sender; + this.displayUniqueId = displayUniqueId; if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { HandlerThread thread = new HandlerThread("UHidManager"); thread.start(); @@ -52,15 +59,22 @@ public final class UhidManager { try { FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); try { + // First UHID device added + boolean firstDevice = fds.isEmpty(); + FileDescriptor old = fds.put(id, fd); if (old != null) { Ln.w("Duplicate UHID id: " + id); close(old); } - byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc); + String phys = mustUseInputPort() ? INPUT_PORT : null; + byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc, phys); Os.write(fd, req, 0, req.length); + if (firstDevice) { + addUniqueIdAssociation(); + } registerUhidListener(id, fd); } catch (Exception e) { close(fd); @@ -148,7 +162,7 @@ public final class UhidManager { } } - private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) { + private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc, String phys) { /* * struct uhid_event { * uint32_t type; @@ -170,17 +184,23 @@ public final class UhidManager { * } __attribute__((__packed__)); */ - byte[] empty = new byte[256]; ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); buf.putInt(UHID_CREATE2); String actualName = name.isEmpty() ? "scrcpy" : name; - byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); - int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); - assert len <= 127; - buf.put(utf8Name, 0, len); - buf.put(empty, 0, 256 - len); + byte[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8); + int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127); + assert nameLen <= 127; + buf.put(nameBytes, 0, nameLen); + if (phys != null) { + buf.position(4 + 128); + byte[] physBytes = phys.getBytes(StandardCharsets.US_ASCII); + assert physBytes.length <= 63; + buf.put(physBytes); + } + + buf.position(4 + 256); buf.putShort((short) reportDesc.length); buf.putShort(BUS_VIRTUAL); buf.putInt(vendorId); @@ -219,15 +239,26 @@ public final class UhidManager { if (fd != null) { unregisterUhidListener(fd); close(fd); + + if (fds.isEmpty()) { + // Last UHID device removed + removeUniqueIdAssociation(); + } } else { Ln.w("Closing unknown UHID device: " + id); } } public void closeAll() { + if (fds.isEmpty()) { + return; + } + for (FileDescriptor fd : fds.values()) { close(fd); } + + removeUniqueIdAssociation(); } private static void close(FileDescriptor fd) { @@ -237,4 +268,20 @@ public final class UhidManager { Ln.e("Failed to close uhid: " + e.getMessage()); } } + + private boolean mustUseInputPort() { + return Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15 && displayUniqueId != null; + } + + private void addUniqueIdAssociation() { + if (mustUseInputPort()) { + ServiceManager.getInputManager().addUniqueIdAssociationByPort(INPUT_PORT, displayUniqueId); + } + } + + private void removeUniqueIdAssociation() { + if (mustUseInputPort()) { + ServiceManager.getInputManager().removeUniqueIdAssociationByPort(INPUT_PORT); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java index cdd4bab9..8d26b7ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java @@ -7,16 +7,18 @@ public final class DisplayInfo { private final int layerStack; private final int flags; private final int dpi; + private final String uniqueId; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; - public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) { + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi, String uniqueId) { this.displayId = displayId; this.size = size; this.rotation = rotation; this.layerStack = layerStack; this.flags = flags; this.dpi = dpi; + this.uniqueId = uniqueId; } public int getDisplayId() { @@ -42,5 +44,8 @@ public final class DisplayInfo { public int getDpi() { return dpi; } -} + public String getUniqueId() { + return uniqueId; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 2f86bbd2..a12470a4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -82,7 +82,7 @@ public final class DisplayManager { int density = Integer.parseInt(m.group(5)); int layerStack = Integer.parseInt(m.group(6)); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null); } private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { @@ -139,7 +139,8 @@ public final class DisplayManager { int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); int flags = cls.getDeclaredField("flags").getInt(displayInfo); int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi); + String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 24c5f80c..f55648d5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,9 +1,11 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.view.InputEvent; import android.view.MotionEvent; @@ -23,6 +25,8 @@ public final class InputManager { private static Method injectInputEventMethod; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; + private static Method addUniqueIdAssociationByPortMethod; + private static Method removeUniqueIdAssociationByPortMethod; static InputManager create() { android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() @@ -103,4 +107,40 @@ public final class InputManager { return false; } } + + private static Method getAddUniqueIdAssociationByPortMethod() throws NoSuchMethodException { + if (addUniqueIdAssociationByPortMethod == null) { + addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( + "addUniqueIdAssociationByPort", String.class, String.class); + } + return addUniqueIdAssociationByPortMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public void addUniqueIdAssociationByPort(String inputPort, String uniqueId) { + try { + Method method = getAddUniqueIdAssociationByPortMethod(); + method.invoke(manager, inputPort, uniqueId); + } catch (ReflectiveOperationException e) { + Ln.e("Cannot add unique id association by port", e); + } + } + + private static Method getRemoveUniqueIdAssociationByPortMethod() throws NoSuchMethodException { + if (removeUniqueIdAssociationByPortMethod == null) { + removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( + "removeUniqueIdAssociationByPort", String.class); + } + return removeUniqueIdAssociationByPortMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public void removeUniqueIdAssociationByPort(String inputPort) { + try { + Method method = getRemoveUniqueIdAssociationByPortMethod(); + method.invoke(manager, inputPort); + } catch (ReflectiveOperationException e) { + Ln.e("Cannot remove unique id association by port", e); + } + } }