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 5e64a4c5..e44ca731 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -148,6 +148,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { displayDataAvailable.notify(); } } + getUhidManager().setDisplayId(virtualDisplayId); } public void setSurfaceCapture(SurfaceCapture surfaceCapture) { 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..6a59a3ac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -1,8 +1,10 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.device.DisplayInfo; 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; @@ -21,6 +23,32 @@ import java.nio.charset.StandardCharsets; public final class UhidManager { + static class Device { + String inputPort; + FileDescriptor fd; + + Device(String inputPort, FileDescriptor fd, int displayId) { + this.inputPort = inputPort; + this.fd = fd; + setDisplayId(displayId); + } + + void setDisplayId(int displayId) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14 && displayId != 0) { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + ServiceManager.getInputManager().addUniqueIdAssociationByPort(inputPort, displayInfo.getUniqueId()); + } + } + + void close() { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { + ServiceManager.getInputManager().removeUniqueIdAssociationByPort(inputPort); + } + + UhidManager.close(fd); + } + } + // Linux: include/uapi/linux/uhid.h private static final int UHID_OUTPUT = 6; private static final int UHID_CREATE2 = 11; @@ -31,12 +59,14 @@ public final class UhidManager { private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event) - private final ArrayMap fds = new ArrayMap<>(); + private final ArrayMap devices = new ArrayMap<>(); private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final DeviceMessageSender sender; private final MessageQueue queue; + private int displayId = 0; + public UhidManager(DeviceMessageSender sender) { this.sender = sender; if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { @@ -52,13 +82,15 @@ public final class UhidManager { try { FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); try { - FileDescriptor old = fds.put(id, fd); + // Must be unique across the system + String inputPort = "scrcpy:" + Os.getpid() + ":" + id; + Device old = devices.put(id, new Device(inputPort, fd, displayId)); if (old != null) { Ln.w("Duplicate UHID id: " + id); - close(old); + old.close(); } - byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc); + byte[] req = buildUhidCreate2Req(vendorId, productId, name, inputPort, reportDesc); Os.write(fd, req, 0, req.length); registerUhidListener(id, fd); @@ -71,6 +103,13 @@ public final class UhidManager { } } + public void setDisplayId(int displayId) { + this.displayId = displayId; + for (Device device : devices.values()) { + device.setDisplayId(displayId); + } + } + private void registerUhidListener(int id, FileDescriptor fd) { if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { queue.addOnFileDescriptorEventListener(fd, MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT, (fd2, events) -> { @@ -134,21 +173,21 @@ public final class UhidManager { } public void writeInput(int id, byte[] data) throws IOException { - FileDescriptor fd = fds.get(id); - if (fd == null) { + Device device = devices.get(id); + if (device == null) { Ln.w("Unknown UHID id: " + id); return; } try { byte[] req = buildUhidInput2Req(data); - Os.write(fd, req, 0, req.length); + Os.write(device.fd, req, 0, req.length); } catch (ErrnoException e) { throw new IOException(e); } } - private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) { + private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, String phys, byte[] reportDesc) { /* * struct uhid_event { * uint32_t type; @@ -170,16 +209,21 @@ 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); + buf.put(nameBytes, 0, nameLen); + buf.position(buf.position() + 128 - nameLen); + + byte[] physBytes = phys.getBytes(StandardCharsets.UTF_8); + int physLen = StringUtils.getUtf8TruncationIndex(physBytes, 63); + buf.put(physBytes, 0, physLen); + buf.position(buf.position() + 64 - physLen); + + buf.position(buf.position() + 64); // uniq buf.putShort((short) reportDesc.length); buf.putShort(BUS_VIRTUAL); @@ -215,18 +259,18 @@ public final class UhidManager { public void close(int id) { // Linux: Documentation/hid/uhid.rst // If you close() the fd, the device is automatically unregistered and destroyed internally. - FileDescriptor fd = fds.remove(id); - if (fd != null) { - unregisterUhidListener(fd); - close(fd); + Device device = devices.remove(id); + if (device != null) { + unregisterUhidListener(device.fd); + device.close(); } else { Ln.w("Closing unknown UHID device: " + id); } } public void closeAll() { - for (FileDescriptor fd : fds.values()) { - close(fd); + for (Device device : devices.values()) { + device.close(); } } 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..9496c46e 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,9 @@ 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 d44ac608..9c1661bf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -81,7 +81,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, ""); } private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { @@ -129,7 +129,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 5c5ba56c..d72a106d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,8 +1,10 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; +import android.content.Context; import android.view.InputEvent; import android.view.MotionEvent; @@ -16,38 +18,25 @@ public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; private final Object manager; - private Method injectInputEventMethod; + private static Method injectInputEventMethod; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; + private static Method addUniqueIdAssociationByPortMethod; + private static Method removeUniqueIdAssociationByPortMethod; static InputManager create() { - try { - Class inputManagerClass = getInputManagerClass(); - Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); - Object im = getInstanceMethod.invoke(null); - return new InputManager(im); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } - } - - private static Class getInputManagerClass() { - try { - // Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview - return Class.forName("android.hardware.input.InputManagerGlobal"); - } catch (ClassNotFoundException e) { - return android.hardware.input.InputManager.class; - } + Object im = FakeContext.get().getSystemService(Context.INPUT_SERVICE); + return new InputManager(im); } private InputManager(Object manager) { this.manager = manager; } - private Method getInjectInputEventMethod() throws NoSuchMethodException { + private static Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class); } return injectInputEventMethod; } @@ -97,4 +86,48 @@ public final class InputManager { return false; } } + + private static Method getAddUniqueIdAssociationByPortMethod() throws NoSuchMethodException { + if (addUniqueIdAssociationByPortMethod == null) { + try { + // Android 15 + addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod("addUniqueIdAssociationByPort", String.class, String.class); + } catch (NoSuchMethodException ignored) { + // Android 12 to 14 + addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod("addUniqueIdAssociation", String.class, String.class); + } + } + return addUniqueIdAssociationByPortMethod; + } + + 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) { + try { + // Android 15 + removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod("removeUniqueIdAssociationByPort", String.class); + } catch (NoSuchMethodException ignored) { + // Android 12 to 14 + removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod("removeUniqueIdAssociation", String.class); + } + } + return removeUniqueIdAssociationByPortMethod; + } + + 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); + } + } }