Compare commits

...

9 Commits

Author SHA1 Message Date
Simon Chan
8de03156d5 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>
2025-04-25 16:49:12 +02:00
Simon Chan
2912ab0421 Simplify InputManager wrapper
Use the public InputManager API.

TODO refs 6009

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-04-25 16:46:15 +02:00
Romain Vimont
e35bbe81e4 Simplify ClipboardManager wrapper
Use the public ClipboardManager API, with the FakeContext as context.

TODO refs 6009

Suggested by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2025-04-25 16:46:10 +02:00
Romain Vimont
91a4a74641 Move regex pattern initialization
If text == null, then the Pattern is not used.
2025-04-25 10:24:07 +02:00
Romain Vimont
48f38c4bb6 Fix default locked capture orientation
The default landscape locked orientation was reversed.

Fixes #6010 <https://github.com/Genymobile/scrcpy/issues/6010>
2025-04-24 16:12:28 +02:00
Romain Vimont
6875e9aa88 Revert "Fix AudioRecord package name for Android 16"
This reverts commit c27d116a66.

This commit breaks audio on Android 16 beta 4.

Refs #5960 comment <https://github.com/Genymobile/scrcpy/issues/5960#issuecomment-2816608015>
Fixes #6021 <https://github.com/Genymobile/scrcpy/issues/6021>
2025-04-24 16:05:13 +02:00
Romain Vimont
1a0d300786 Add missing --screen-off-timeout doc in manpage
Refs eff5b4b219
2025-04-14 18:07:37 +02:00
Romain Vimont
d2447b5c19 Fix --screen-off-timeout bash completion
Only the option must be auto-completed, not its value.
2025-04-14 18:05:08 +02:00
Romain Vimont
882003f314 Fix segfault on SDL event without window
Since #5804, controls have been enabled even with --no-window. As a
result, the Android clipboard is synchronized with the computer, causing
SDL to trigger an SDL_CLIPBOARDUPDATE event.

This event is ignored by scrcpy, but it was still transmitted to the
sc_screen instance, even if it had not been initialized.

Fix the issue by calling sc_screen_handle_event() only when a screen
instance exists.

Refs #5804 <https://github.com/Genymobile/scrcpy/pull/5804>
Fixes #5970 <https://github.com/Genymobile/scrcpy/issues/5970>
2025-04-03 08:15:55 +02:00
14 changed files with 209 additions and 304 deletions

View File

@@ -205,6 +205,7 @@ _scrcpy() {
|-p|--port \
|--push-target \
|--rotation \
|--screen-off-timeout \
|--tunnel-host \
|--tunnel-port \
|--v4l2-buffer \

View File

@@ -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".

View File

@@ -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...");

View File

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

View File

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

View File

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

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,8 @@ public final class DisplayInfo {
public int getDpi() {
return dpi;
}
}
public String getUniqueId() {
return uniqueId;
}
}

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

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

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

View File

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