Associate UHID devices to virtual displays on Android 14+

This commit is contained in:
Simon Chan 2025-04-16 14:26:24 +08:00
parent 1a0d300786
commit 0ef8f64165
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
5 changed files with 128 additions and 43 deletions

View file

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

View file

@ -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<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 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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}