mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-08-03 14:49:29 +00:00
Merge 0ef8f64165
into 1a0d300786
This commit is contained in:
commit
b1e4c3e205
5 changed files with 128 additions and 43 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue