This commit is contained in:
Simon Chan 2025-04-16 06:38:05 +00:00 committed by GitHub
commit b1e4c3e205
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 128 additions and 43 deletions

View file

@ -148,6 +148,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
displayDataAvailable.notify(); displayDataAvailable.notify();
} }
} }
getUhidManager().setDisplayId(virtualDisplayId);
} }
public void setSurfaceCapture(SurfaceCapture surfaceCapture) { public void setSurfaceCapture(SurfaceCapture surfaceCapture) {

View file

@ -1,8 +1,10 @@
package com.genymobile.scrcpy.control; package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils; import com.genymobile.scrcpy.util.StringUtils;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build; import android.os.Build;
import android.os.HandlerThread; import android.os.HandlerThread;
@ -21,6 +23,32 @@ import java.nio.charset.StandardCharsets;
public final class UhidManager { 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 // Linux: include/uapi/linux/uhid.h
private static final int UHID_OUTPUT = 6; private static final int UHID_OUTPUT = 6;
private static final int UHID_CREATE2 = 11; 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 static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event)
private final ArrayMap<Integer, FileDescriptor> fds = new ArrayMap<>(); private final ArrayMap<Integer, Device> devices = new ArrayMap<>();
private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder());
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
private final MessageQueue queue; private final MessageQueue queue;
private int displayId = 0;
public UhidManager(DeviceMessageSender sender) { public UhidManager(DeviceMessageSender sender) {
this.sender = sender; this.sender = sender;
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) {
@ -52,13 +82,15 @@ public final class UhidManager {
try { try {
FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0);
try { 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) { if (old != null) {
Ln.w("Duplicate UHID id: " + id); 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); Os.write(fd, req, 0, req.length);
registerUhidListener(id, fd); 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) { private void registerUhidListener(int id, FileDescriptor fd) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) {
queue.addOnFileDescriptorEventListener(fd, MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT, (fd2, events) -> { 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 { public void writeInput(int id, byte[] data) throws IOException {
FileDescriptor fd = fds.get(id); Device device = devices.get(id);
if (fd == null) { if (device == null) {
Ln.w("Unknown UHID id: " + id); Ln.w("Unknown UHID id: " + id);
return; return;
} }
try { try {
byte[] req = buildUhidInput2Req(data); byte[] req = buildUhidInput2Req(data);
Os.write(fd, req, 0, req.length); Os.write(device.fd, req, 0, req.length);
} catch (ErrnoException e) { } catch (ErrnoException e) {
throw new IOException(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 { * struct uhid_event {
* uint32_t type; * uint32_t type;
@ -170,16 +209,21 @@ public final class UhidManager {
* } __attribute__((__packed__)); * } __attribute__((__packed__));
*/ */
byte[] empty = new byte[256];
ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder());
buf.putInt(UHID_CREATE2); buf.putInt(UHID_CREATE2);
String actualName = name.isEmpty() ? "scrcpy" : name; String actualName = name.isEmpty() ? "scrcpy" : name;
byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); byte[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127);
assert len <= 127; buf.put(nameBytes, 0, nameLen);
buf.put(utf8Name, 0, len); buf.position(buf.position() + 128 - nameLen);
buf.put(empty, 0, 256 - len);
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((short) reportDesc.length);
buf.putShort(BUS_VIRTUAL); buf.putShort(BUS_VIRTUAL);
@ -215,18 +259,18 @@ public final class UhidManager {
public void close(int id) { public void close(int id) {
// Linux: Documentation/hid/uhid.rst // Linux: Documentation/hid/uhid.rst
// If you close() the fd, the device is automatically unregistered and destroyed internally. // If you close() the fd, the device is automatically unregistered and destroyed internally.
FileDescriptor fd = fds.remove(id); Device device = devices.remove(id);
if (fd != null) { if (device != null) {
unregisterUhidListener(fd); unregisterUhidListener(device.fd);
close(fd); device.close();
} else { } else {
Ln.w("Closing unknown UHID device: " + id); Ln.w("Closing unknown UHID device: " + id);
} }
} }
public void closeAll() { public void closeAll() {
for (FileDescriptor fd : fds.values()) { for (Device device : devices.values()) {
close(fd); device.close();
} }
} }

View file

@ -7,16 +7,18 @@ public final class DisplayInfo {
private final int layerStack; private final int layerStack;
private final int flags; private final int flags;
private final int dpi; private final int dpi;
private final String uniqueId;
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; 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.displayId = displayId;
this.size = size; this.size = size;
this.rotation = rotation; this.rotation = rotation;
this.layerStack = layerStack; this.layerStack = layerStack;
this.flags = flags; this.flags = flags;
this.dpi = dpi; this.dpi = dpi;
this.uniqueId = uniqueId;
} }
public int getDisplayId() { public int getDisplayId() {
@ -42,5 +44,9 @@ public final class DisplayInfo {
public int getDpi() { public int getDpi() {
return dpi; return dpi;
} }
public String getUniqueId() {
return uniqueId;
}
} }

View file

@ -81,7 +81,7 @@ public final class DisplayManager {
int density = Integer.parseInt(m.group(5)); int density = Integer.parseInt(m.group(5));
int layerStack = Integer.parseInt(m.group(6)); 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) { private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
@ -129,7 +129,8 @@ public final class DisplayManager {
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo); int flags = cls.getDeclaredField("flags").getInt(displayInfo);
int dpi = cls.getDeclaredField("logicalDensityDpi").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) { } catch (ReflectiveOperationException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }

View file

@ -1,8 +1,10 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context;
import android.view.InputEvent; import android.view.InputEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
@ -16,38 +18,25 @@ public final class InputManager {
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
private final Object manager; private final Object manager;
private Method injectInputEventMethod;
private static Method injectInputEventMethod;
private static Method setDisplayIdMethod; private static Method setDisplayIdMethod;
private static Method setActionButtonMethod; private static Method setActionButtonMethod;
private static Method addUniqueIdAssociationByPortMethod;
private static Method removeUniqueIdAssociationByPortMethod;
static InputManager create() { static InputManager create() {
try { Object im = FakeContext.get().getSystemService(Context.INPUT_SERVICE);
Class<?> inputManagerClass = getInputManagerClass();
Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
Object im = getInstanceMethod.invoke(null);
return new InputManager(im); 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;
}
} }
private InputManager(Object manager) { private InputManager(Object manager) {
this.manager = manager; this.manager = manager;
} }
private Method getInjectInputEventMethod() throws NoSuchMethodException { private static Method getInjectInputEventMethod() throws NoSuchMethodException {
if (injectInputEventMethod == null) { 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; return injectInputEventMethod;
} }
@ -97,4 +86,48 @@ public final class InputManager {
return false; 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);
}
}
} }