mirror of
https://github.com/Genymobile/scrcpy
synced 2025-08-22 09:57:30 +00:00
Associate UHID devices to virtual displays
This allows the mouse pointer to appear on the correct display (only for devices running Android 15+). TODO refs 6009. Signed-off-by: Romain Vimont <rom@rom1v.com>
This commit is contained in:
parent
2912ab0421
commit
8de03156d5
@ -54,10 +54,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private static final class DisplayData {
|
||||
private final int virtualDisplayId;
|
||||
private final String displayUniqueId;
|
||||
private final PositionMapper positionMapper;
|
||||
|
||||
private DisplayData(int virtualDisplayId, PositionMapper positionMapper) {
|
||||
private DisplayData(int virtualDisplayId, String displayUniqueId, PositionMapper positionMapper) {
|
||||
this.virtualDisplayId = virtualDisplayId;
|
||||
this.displayUniqueId = displayUniqueId;
|
||||
this.positionMapper = positionMapper;
|
||||
}
|
||||
}
|
||||
@ -135,8 +137,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewVirtualDisplay(int virtualDisplayId, PositionMapper positionMapper) {
|
||||
DisplayData data = new DisplayData(virtualDisplayId, positionMapper);
|
||||
public void onNewVirtualDisplay(int virtualDisplayId, String displayUniqueId, PositionMapper positionMapper) {
|
||||
DisplayData data = new DisplayData(virtualDisplayId, displayUniqueId, positionMapper);
|
||||
DisplayData old = this.displayData.getAndSet(data);
|
||||
if (old == null) {
|
||||
// The very first time the Controller is notified of a new virtual display
|
||||
@ -152,7 +154,21 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private UhidManager getUhidManager() {
|
||||
if (uhidManager == null) {
|
||||
uhidManager = new UhidManager(sender);
|
||||
String displayUniqueId = null;
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15 && 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) {
|
||||
displayUniqueId = data.displayUniqueId;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
uhidManager = new UhidManager(sender, displayUniqueId);
|
||||
}
|
||||
return uhidManager;
|
||||
}
|
||||
|
@ -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<Integer, FileDescriptor> 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, displayUniqueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -220,8 +220,17 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
}
|
||||
|
||||
if (vdListener != null) {
|
||||
int displayId = virtualDisplay.getDisplay().getDisplayId();
|
||||
String displayUniqueId = null;
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) {
|
||||
// The display unique id is not used before Android 15
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (displayInfo != null) {
|
||||
displayUniqueId = displayInfo.getUniqueId();
|
||||
}
|
||||
}
|
||||
PositionMapper positionMapper = PositionMapper.create(videoSize, eventTransform, displaySize);
|
||||
vdListener.onNewVirtualDisplay(virtualDisplay.getDisplay().getDisplayId(), positionMapper);
|
||||
vdListener.onNewVirtualDisplay(displayId, displayUniqueId, positionMapper);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,7 +156,7 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
positionMapper = PositionMapper.create(videoSize, transform, inputSize);
|
||||
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
||||
}
|
||||
vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper);
|
||||
vdListener.onNewVirtualDisplay(virtualDisplayId, displayInfo.getUniqueId(), positionMapper);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,5 +3,5 @@ package com.genymobile.scrcpy.video;
|
||||
import com.genymobile.scrcpy.control.PositionMapper;
|
||||
|
||||
public interface VirtualDisplayListener {
|
||||
void onNewVirtualDisplay(int displayId, PositionMapper positionMapper);
|
||||
void onNewVirtualDisplay(int displayId, String displayUniqueId, PositionMapper positionMapper);
|
||||
}
|
||||
|
@ -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, null);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
@ -20,6 +22,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()
|
||||
@ -83,4 +87,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, String uniqueId) {
|
||||
try {
|
||||
Method method = getRemoveUniqueIdAssociationByPortMethod();
|
||||
method.invoke(manager, inputPort);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Cannot remove unique id association by port", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user