mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-02-23 23:04:43 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8de03156d5 | ||
|
|
2912ab0421 | ||
|
|
e35bbe81e4 | ||
|
|
91a4a74641 | ||
|
|
48f38c4bb6 | ||
|
|
6875e9aa88 | ||
|
|
1a0d300786 | ||
|
|
d2447b5c19 | ||
|
|
882003f314 |
@@ -205,6 +205,7 @@ _scrcpy() {
|
||||
|-p|--port \
|
||||
|--push-target \
|
||||
|--rotation \
|
||||
|--screen-off-timeout \
|
||||
|--tunnel-host \
|
||||
|--tunnel-port \
|
||||
|--v4l2-buffer \
|
||||
|
||||
@@ -510,6 +510,10 @@ The device serial number. Mandatory only if several devices are connected to adb
|
||||
.B \-S, \-\-turn\-screen\-off
|
||||
Turn the device screen off immediately.
|
||||
|
||||
.TP
|
||||
.B "\-\-screen\-off\-timeout " seconds
|
||||
Set the screen off timeout while scrcpy is running (restore the initial value on exit).
|
||||
|
||||
.TP
|
||||
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
|
||||
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
|
||||
|
||||
@@ -165,7 +165,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) {
|
||||
}
|
||||
|
||||
static enum scrcpy_exit_code
|
||||
event_loop(struct scrcpy *s) {
|
||||
event_loop(struct scrcpy *s, bool has_screen) {
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
@@ -197,7 +197,7 @@ event_loop(struct scrcpy *s) {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (!sc_screen_handle_event(&s->screen, &event)) {
|
||||
if (has_screen && !sc_screen_handle_event(&s->screen, &event)) {
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
}
|
||||
break;
|
||||
@@ -933,7 +933,7 @@ aoa_complete:
|
||||
}
|
||||
}
|
||||
|
||||
ret = event_loop(s);
|
||||
ret = event_loop(s, options->window);
|
||||
terminate_event_loop();
|
||||
LOGD("quit...");
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.AttributionSource;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
@@ -11,6 +13,8 @@ import android.content.IContentProvider;
|
||||
import android.os.Binder;
|
||||
import android.os.Process;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public final class FakeContext extends ContextWrapper {
|
||||
|
||||
public static final String PACKAGE_NAME = "com.android.shell";
|
||||
@@ -72,7 +76,7 @@ public final class FakeContext extends ContextWrapper {
|
||||
@Override
|
||||
public AttributionSource getAttributionSource() {
|
||||
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
|
||||
builder.setPackageName("shell");
|
||||
builder.setPackageName(PACKAGE_NAME);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@@ -91,4 +95,25 @@ public final class FakeContext extends ContextWrapper {
|
||||
public ContentResolver getContentResolver() {
|
||||
return contentResolver;
|
||||
}
|
||||
|
||||
@SuppressLint("SoonBlockedPrivateApi")
|
||||
@Override
|
||||
public Object getSystemService(String name) {
|
||||
Object service = super.getSystemService(name);
|
||||
if (service == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Context.CLIPBOARD_SERVICE.equals(name)) {
|
||||
try {
|
||||
Field field = ClipboardManager.class.getDeclaredField("mContext");
|
||||
field.setAccessible(true);
|
||||
field.set(service, this);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
@@ -55,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;
|
||||
}
|
||||
}
|
||||
@@ -118,18 +119,15 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
|
||||
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
|
||||
if (clipboardManager != null) {
|
||||
clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
|
||||
@Override
|
||||
public void dispatchPrimaryClipChanged() {
|
||||
if (isSettingClipboard.get()) {
|
||||
// This is a notification for the change we are currently applying, ignore it
|
||||
return;
|
||||
}
|
||||
String text = Device.getClipboardText();
|
||||
if (text != null) {
|
||||
DeviceMessage msg = DeviceMessage.createClipboard(text);
|
||||
sender.send(msg);
|
||||
}
|
||||
clipboardManager.addPrimaryClipChangedListener(() -> {
|
||||
if (isSettingClipboard.get()) {
|
||||
// This is a notification for the change we are currently applying, ignore it
|
||||
return;
|
||||
}
|
||||
String text = Device.getClipboardText();
|
||||
if (text != null) {
|
||||
DeviceMessage msg = DeviceMessage.createClipboard(text);
|
||||
sender.send(msg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -139,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
|
||||
@@ -156,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,11 @@ public enum Orientation {
|
||||
throw new IllegalArgumentException("Unknown orientation: " + name);
|
||||
}
|
||||
|
||||
public static Orientation fromRotation(int rotation) {
|
||||
assert rotation >= 0 && rotation < 4;
|
||||
return values()[rotation];
|
||||
public static Orientation fromRotation(int ccwRotation) {
|
||||
assert ccwRotation >= 0 && ccwRotation < 4;
|
||||
// Display rotation is expressed counter-clockwise, orientation is expressed clockwise
|
||||
int cwRotation = (4 - ccwRotation) % 4;
|
||||
return values()[cwRotation];
|
||||
}
|
||||
|
||||
public boolean isFlipped() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,270 +1,43 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.FakeContext;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class ClipboardManager {
|
||||
private final IInterface manager;
|
||||
private Method getPrimaryClipMethod;
|
||||
private Method setPrimaryClipMethod;
|
||||
private Method addPrimaryClipChangedListener;
|
||||
private int getMethodVersion;
|
||||
private int setMethodVersion;
|
||||
private int addListenerMethodVersion;
|
||||
private final android.content.ClipboardManager manager;
|
||||
|
||||
static ClipboardManager create() {
|
||||
IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard");
|
||||
if (clipboard == null) {
|
||||
android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get()
|
||||
.getSystemService(FakeContext.CLIPBOARD_SERVICE);
|
||||
if (manager == null) {
|
||||
// Some devices have no clipboard manager
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1440>
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1556>
|
||||
return null;
|
||||
}
|
||||
return new ClipboardManager(clipboard);
|
||||
return new ClipboardManager(manager);
|
||||
}
|
||||
|
||||
private ClipboardManager(IInterface manager) {
|
||||
private ClipboardManager(android.content.ClipboardManager manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
|
||||
if (getPrimaryClipMethod == null) {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
|
||||
return getPrimaryClipMethod;
|
||||
}
|
||||
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
|
||||
getMethodVersion = 0;
|
||||
return getPrimaryClipMethod;
|
||||
} catch (NoSuchMethodException e) {
|
||||
// fall-through
|
||||
}
|
||||
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
|
||||
getMethodVersion = 1;
|
||||
return getPrimaryClipMethod;
|
||||
} catch (NoSuchMethodException e) {
|
||||
// fall-through
|
||||
}
|
||||
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
|
||||
getMethodVersion = 2;
|
||||
return getPrimaryClipMethod;
|
||||
} catch (NoSuchMethodException e) {
|
||||
// fall-through
|
||||
}
|
||||
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
|
||||
getMethodVersion = 3;
|
||||
return getPrimaryClipMethod;
|
||||
} catch (NoSuchMethodException e) {
|
||||
// fall-through
|
||||
}
|
||||
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass()
|
||||
.getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
|
||||
getMethodVersion = 4;
|
||||
return getPrimaryClipMethod;
|
||||
} catch (NoSuchMethodException e) {
|
||||
// fall-through
|
||||
}
|
||||
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass()
|
||||
.getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class);
|
||||
getMethodVersion = 5;
|
||||
return getPrimaryClipMethod;
|
||||
} catch (NoSuchMethodException e) {
|
||||
// fall-through
|
||||
}
|
||||
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class);
|
||||
getMethodVersion = 6;
|
||||
}
|
||||
return getPrimaryClipMethod;
|
||||
}
|
||||
|
||||
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
|
||||
if (setPrimaryClipMethod == null) {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
|
||||
return setPrimaryClipMethod;
|
||||
}
|
||||
|
||||
try {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
|
||||
setMethodVersion = 0;
|
||||
return setPrimaryClipMethod;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
// fall-through
|
||||
}
|
||||
|
||||
try {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
|
||||
setMethodVersion = 1;
|
||||
return setPrimaryClipMethod;
|
||||
} catch (NoSuchMethodException e2) {
|
||||
// fall-through
|
||||
}
|
||||
|
||||
try {
|
||||
setPrimaryClipMethod = manager.getClass()
|
||||
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
|
||||
setMethodVersion = 2;
|
||||
return setPrimaryClipMethod;
|
||||
} catch (NoSuchMethodException e3) {
|
||||
// fall-through
|
||||
}
|
||||
|
||||
setPrimaryClipMethod = manager.getClass()
|
||||
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class);
|
||||
setMethodVersion = 3;
|
||||
}
|
||||
return setPrimaryClipMethod;
|
||||
}
|
||||
|
||||
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
|
||||
}
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
case 1:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
case 2:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
case 3:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
|
||||
case 4:
|
||||
// The last boolean parameter is "userOperate"
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
|
||||
case 5:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true);
|
||||
default:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
break;
|
||||
case 1:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
break;
|
||||
case 2:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
break;
|
||||
default:
|
||||
// The last boolean parameter is "userOperate"
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
public CharSequence getText() {
|
||||
try {
|
||||
Method method = getGetPrimaryClipMethod();
|
||||
ClipData clipData = getPrimaryClip(method, getMethodVersion, manager);
|
||||
if (clipData == null || clipData.getItemCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
return clipData.getItemAt(0).getText();
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
ClipData clipData = manager.getPrimaryClip();
|
||||
if (clipData == null || clipData.getItemCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
return clipData.getItemAt(0).getText();
|
||||
}
|
||||
|
||||
public boolean setText(CharSequence text) {
|
||||
try {
|
||||
Method method = getSetPrimaryClipMethod();
|
||||
ClipData clipData = ClipData.newPlainText(null, text);
|
||||
setPrimaryClip(method, setMethodVersion, manager, clipData);
|
||||
return true;
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
ClipData clipData = ClipData.newPlainText(null, text);
|
||||
manager.setPrimaryClip(clipData);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener)
|
||||
throws ReflectiveOperationException {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
break;
|
||||
case 1:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
break;
|
||||
default:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
|
||||
if (addPrimaryClipChangedListener == null) {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
|
||||
} else {
|
||||
try {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
|
||||
addListenerMethodVersion = 0;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
try {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
|
||||
int.class);
|
||||
addListenerMethodVersion = 1;
|
||||
} catch (NoSuchMethodException e2) {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
|
||||
int.class, int.class);
|
||||
addListenerMethodVersion = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return addPrimaryClipChangedListener;
|
||||
}
|
||||
|
||||
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
|
||||
try {
|
||||
Method method = getAddPrimaryClipChangedListener();
|
||||
addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
|
||||
return true;
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) {
|
||||
manager.addPrimaryClipChangedListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -95,12 +95,12 @@ public final class DisplayManager {
|
||||
}
|
||||
|
||||
private static int parseDisplayFlags(String text) {
|
||||
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
|
||||
if (text == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int flags = 0;
|
||||
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
|
||||
Matcher m = regex.matcher(text);
|
||||
while (m.find()) {
|
||||
String flagString = m.group();
|
||||
@@ -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,8 +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;
|
||||
|
||||
@@ -15,39 +18,26 @@ public final class InputManager {
|
||||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
|
||||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
|
||||
|
||||
private final Object manager;
|
||||
private Method injectInputEventMethod;
|
||||
|
||||
private final android.hardware.input.InputManager manager;
|
||||
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);
|
||||
}
|
||||
android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get()
|
||||
.getSystemService(FakeContext.INPUT_SERVICE);
|
||||
return new InputManager(manager);
|
||||
}
|
||||
|
||||
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(android.hardware.input.InputManager manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
private 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 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user