mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-04-21 01:33:36 +00:00
Merge 3c52506fa7 into 175134c0ca
This commit is contained in:
commit
94f2ce9dd3
15 changed files with 521 additions and 9 deletions
|
|
@ -1,5 +1,6 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
|
@ -9,10 +10,13 @@ import android.content.ContentResolver;
|
|||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.IContentProvider;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Binder;
|
||||
import android.os.Process;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class FakeContext extends ContextWrapper {
|
||||
|
||||
|
|
@ -25,6 +29,29 @@ public final class FakeContext extends ContextWrapper {
|
|||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static Context getSystemContext() {
|
||||
Context base = Workarounds.getSystemContext();
|
||||
|
||||
try {
|
||||
// `getFilesDir()` method requires correct app info
|
||||
PackageManager pm = base.getPackageManager();
|
||||
ApplicationInfo appInfo = pm.getApplicationInfo(PACKAGE_NAME, 0);
|
||||
|
||||
Field mPackageInfoField = base.getClass().getDeclaredField("mPackageInfo");
|
||||
mPackageInfoField.setAccessible(true);
|
||||
Object mPackageInfo = mPackageInfoField.get(base);
|
||||
|
||||
Method setApplicationInfoMethod = mPackageInfo.getClass().getDeclaredMethod("setApplicationInfo", ApplicationInfo.class);
|
||||
setApplicationInfoMethod.setAccessible(true);
|
||||
setApplicationInfoMethod.invoke(mPackageInfo, appInfo);
|
||||
|
||||
return base;
|
||||
} catch (Exception e) {
|
||||
Ln.w("Can't set application info to base context" , e);
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
private final ContentResolver contentResolver = new ContentResolver(this) {
|
||||
@SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
|
||||
// @Override (but super-class method not visible)
|
||||
|
|
@ -41,7 +68,7 @@ public final class FakeContext extends ContextWrapper {
|
|||
@SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
|
||||
// @Override (but super-class method not visible)
|
||||
protected IContentProvider acquireUnstableProvider(Context c, String name) {
|
||||
return null;
|
||||
return ServiceManager.getActivityManager().getContentProviderExternal(name, new Binder());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
|
@ -58,7 +85,8 @@ public final class FakeContext extends ContextWrapper {
|
|||
};
|
||||
|
||||
private FakeContext() {
|
||||
super(Workarounds.getSystemContext());
|
||||
super(getSystemContext());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public final class ControlMessage {
|
|||
public static final int TYPE_CAMERA_SET_TORCH = 18;
|
||||
public static final int TYPE_CAMERA_ZOOM_IN = 19;
|
||||
public static final int TYPE_CAMERA_ZOOM_OUT = 20;
|
||||
public static final int TYPE_SET_IMAGE_CLIPBOARD = 21;
|
||||
|
||||
public static final long SEQUENCE_INVALID = 0;
|
||||
|
||||
|
|
@ -176,6 +177,16 @@ public final class ControlMessage {
|
|||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createSetImageClipboard(long sequence, boolean paste, String mimeType, byte[] data) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_SET_IMAGE_CLIPBOARD;
|
||||
msg.sequence = sequence;
|
||||
msg.paste = paste;
|
||||
msg.text = mimeType; // Store mimeType in text field temporarily
|
||||
msg.data = data;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ public class ControlMessageReader {
|
|||
return parseStartApp();
|
||||
case ControlMessage.TYPE_CAMERA_SET_TORCH:
|
||||
return parseCameraSetTorch();
|
||||
case ControlMessage.TYPE_SET_IMAGE_CLIPBOARD:
|
||||
return parseSetImageClipboard();
|
||||
default:
|
||||
throw new ControlProtocolException("Unknown event type: " + type);
|
||||
}
|
||||
|
|
@ -175,6 +177,14 @@ public class ControlMessageReader {
|
|||
return ControlMessage.createCameraSetTorch(on);
|
||||
}
|
||||
|
||||
private ControlMessage parseSetImageClipboard() throws IOException {
|
||||
long sequence = dis.readLong();
|
||||
boolean paste = dis.readByte() != 0;
|
||||
String mimeType = parseString();
|
||||
byte[] data = parseByteArray(4); // Read image data
|
||||
return ControlMessage.createSetImageClipboard(sequence, paste, mimeType, data);
|
||||
}
|
||||
|
||||
private Position parsePosition() throws IOException {
|
||||
int x = dis.readInt();
|
||||
int y = dis.readInt();
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import com.genymobile.scrcpy.video.SurfaceCapture;
|
|||
import com.genymobile.scrcpy.video.VideoSource;
|
||||
import com.genymobile.scrcpy.video.VirtualDisplayListener;
|
||||
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||
import com.genymobile.scrcpy.wrappers.ClipboardManager.ClipboardImage;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
|
|
@ -140,10 +141,19 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
// 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);
|
||||
// Check for image clipboard first
|
||||
ClipboardImage clipboardImage = Device.getClipboardImage();
|
||||
if (clipboardImage != null && clipboardImage.data().length > 0) {
|
||||
// Send image clipboard data
|
||||
DeviceMessage msg = DeviceMessage.createImageClipboard(clipboardImage.data(), clipboardImage.mimeType());
|
||||
sender.send(msg);
|
||||
} else {
|
||||
// Fall back to text clipboard
|
||||
String text = Device.getClipboardText();
|
||||
if (text != null) {
|
||||
DeviceMessage msg = DeviceMessage.createClipboard(text);
|
||||
sender.send(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
|
@ -343,6 +353,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
||||
return true;
|
||||
case ControlMessage.TYPE_SET_IMAGE_CLIPBOARD:
|
||||
setImageClipboard(msg.getSequence(), msg.getPaste(), msg.getText(), msg.getData()); // text field contains mimeType
|
||||
return true;
|
||||
case ControlMessage.TYPE_SET_DISPLAY_POWER:
|
||||
if (supportsInputEvents) {
|
||||
setDisplayPower(msg.getOn());
|
||||
|
|
@ -639,10 +652,19 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
// particular when COPY or CUT are injected, so it should not be synchronized twice. On Android < 7, do not synchronize at all rather than
|
||||
// copying an old clipboard content.
|
||||
if (!clipboardAutosync) {
|
||||
String clipboardText = Device.getClipboardText();
|
||||
if (clipboardText != null) {
|
||||
DeviceMessage msg = DeviceMessage.createClipboard(clipboardText);
|
||||
// Try to get image clipboard first
|
||||
ClipboardImage clipboardImage = Device.getClipboardImage();
|
||||
if (clipboardImage != null && clipboardImage.data().length > 0) {
|
||||
// Send image clipboard data
|
||||
DeviceMessage msg = DeviceMessage.createImageClipboard(clipboardImage.data(), clipboardImage.mimeType());
|
||||
sender.send(msg);
|
||||
} else {
|
||||
// Fall back to text clipboard
|
||||
String clipboardText = Device.getClipboardText();
|
||||
if (clipboardText != null) {
|
||||
DeviceMessage msg = DeviceMessage.createClipboard(clipboardText);
|
||||
sender.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -669,6 +691,28 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
return ok;
|
||||
}
|
||||
|
||||
private boolean setImageClipboard(long sequence, boolean paste, String mimeType, byte[] imageData) {
|
||||
isSettingClipboard.set(true);
|
||||
boolean ok = Device.setClipboardImage(imageData, mimeType);
|
||||
isSettingClipboard.set(false);
|
||||
if (ok) {
|
||||
Ln.i("Device image clipboard set");
|
||||
}
|
||||
|
||||
// On Android >= 7, also press the PASTE key if requested
|
||||
if (paste && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && supportsInputEvents) {
|
||||
pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
if (sequence != ControlMessage.SEQUENCE_INVALID) {
|
||||
// Acknowledgement requested
|
||||
DeviceMessage msg = DeviceMessage.createAckClipboard(sequence);
|
||||
sender.send(msg);
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
private void openHardKeyboardSettings() {
|
||||
Intent intent = new Intent("android.settings.HARD_KEYBOARD_SETTINGS");
|
||||
ServiceManager.getActivityManager().startActivity(intent);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ public final class DeviceMessage {
|
|||
public static final int TYPE_CLIPBOARD = 0;
|
||||
public static final int TYPE_ACK_CLIPBOARD = 1;
|
||||
public static final int TYPE_UHID_OUTPUT = 2;
|
||||
public static final int TYPE_IMAGE_CLIPBOARD = 3;
|
||||
|
||||
private int type;
|
||||
private String text;
|
||||
private String mimeType;
|
||||
private long sequence;
|
||||
private int id;
|
||||
private byte[] data;
|
||||
|
|
@ -37,6 +39,14 @@ public final class DeviceMessage {
|
|||
return event;
|
||||
}
|
||||
|
||||
public static DeviceMessage createImageClipboard(byte[] imageData, String mimeType) {
|
||||
DeviceMessage event = new DeviceMessage();
|
||||
event.type = TYPE_IMAGE_CLIPBOARD;
|
||||
event.data = imageData;
|
||||
event.mimeType = mimeType;
|
||||
return event;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
|
@ -56,4 +66,8 @@ public final class DeviceMessage {
|
|||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import com.genymobile.scrcpy.util.StringUtils;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
|
|
@ -39,6 +40,25 @@ public class DeviceMessageWriter {
|
|||
dos.writeShort(data.length);
|
||||
dos.write(data);
|
||||
break;
|
||||
case DeviceMessage.TYPE_IMAGE_CLIPBOARD:
|
||||
byte[] imageData = msg.getData();
|
||||
String mimeType = msg.getMimeType();
|
||||
|
||||
byte[] mimeTypeBytes = mimeType.getBytes(StandardCharsets.UTF_8);
|
||||
int messageSize = 1 + 4 + mimeTypeBytes.length + 4 + imageData.length;
|
||||
if (messageSize > MESSAGE_MAX_SIZE) {
|
||||
Ln.w("Image clipboard message too large: " + messageSize + " bytes, dropping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Write fixed length fields first
|
||||
// so client can easily detect whether they have received the whole message
|
||||
dos.writeInt(mimeTypeBytes.length);
|
||||
dos.writeInt(imageData.length);
|
||||
|
||||
dos.write(mimeTypeBytes);
|
||||
dos.write(imageData);
|
||||
break;
|
||||
default:
|
||||
throw new ControlProtocolException("Unknown event type: " + type);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.genymobile.scrcpy.model.DeviceApp;
|
|||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.wrappers.ActivityManager;
|
||||
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||
import com.genymobile.scrcpy.wrappers.ClipboardManager.ClipboardImage;
|
||||
import com.genymobile.scrcpy.wrappers.DisplayControl;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
|
@ -112,6 +113,14 @@ public final class Device {
|
|||
return s.toString();
|
||||
}
|
||||
|
||||
public static ClipboardImage getClipboardImage() {
|
||||
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
|
||||
if (clipboardManager == null) {
|
||||
return null;
|
||||
}
|
||||
return clipboardManager.getImage();
|
||||
}
|
||||
|
||||
public static boolean setClipboardText(String text) {
|
||||
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
|
||||
if (clipboardManager == null) {
|
||||
|
|
@ -130,6 +139,15 @@ public final class Device {
|
|||
return clipboardManager.setText(text);
|
||||
}
|
||||
|
||||
public static boolean setClipboardImage(byte[] imageData, String mimeType) {
|
||||
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
|
||||
if (clipboardManager == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return clipboardManager.setImage(imageData, mimeType);
|
||||
}
|
||||
|
||||
public static boolean setDisplayPower(int displayId, boolean on) {
|
||||
assert displayId != Device.DISPLAY_ID_NONE;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,48 @@ package com.genymobile.scrcpy.wrappers;
|
|||
|
||||
import com.genymobile.scrcpy.FakeContext;
|
||||
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipDescription;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
public final class ClipboardManager {
|
||||
|
||||
public static class ClipboardImage {
|
||||
private final String mimeType;
|
||||
private final byte[] data;
|
||||
|
||||
public ClipboardImage(String mimeType, byte[] data) {
|
||||
this.mimeType = mimeType;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String mimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public byte[] data() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
private final android.content.ClipboardManager manager;
|
||||
|
||||
private File cachedFolder;
|
||||
|
||||
static ClipboardManager create() {
|
||||
android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (manager == null) {
|
||||
|
|
@ -31,6 +67,85 @@ public final class ClipboardManager {
|
|||
return clipData.getItemAt(0).getText();
|
||||
}
|
||||
|
||||
public ClipboardImage getImage() {
|
||||
ClipData clipData = manager.getPrimaryClip();
|
||||
if (clipData == null || clipData.getItemCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ClipData.Item item = clipData.getItemAt(0);
|
||||
|
||||
// Check if it's an image URI
|
||||
ClipDescription description = clipData.getDescription();
|
||||
if (description != null) {
|
||||
String mimeType = description.filterMimeTypes("image/*")[0];
|
||||
if (mimeType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Uri uri = item.getUri();
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try (InputStream inputStream = FakeContext.get().getContentResolver().openInputStream(uri);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
|
||||
if (inputStream != null) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
return new ClipboardImage(mimeType, outputStream.toByteArray());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Failed to read image data from URI
|
||||
Ln.e("Failed to read clipboard image", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean setImage(byte[] imageData, String mimeType) {
|
||||
try {
|
||||
if (cachedFolder == null) {
|
||||
android.content.Context context = FakeContext.get();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
context = context.createDeviceProtectedStorageContext();
|
||||
}
|
||||
|
||||
File fileRoot = context.getFilesDir();
|
||||
cachedFolder = new File(fileRoot, "bugreports");
|
||||
cachedFolder.mkdirs();
|
||||
}
|
||||
|
||||
// Use atomic write: write to temporary file first, then move to final location
|
||||
File tempFile = new File(cachedFolder, "clipboard.tmp");
|
||||
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
|
||||
fos.write(imageData);
|
||||
}
|
||||
|
||||
File finalFile = new File(cachedFolder, "clipboard");
|
||||
Files.move(tempFile.toPath(), finalFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
android.net.Uri uri = android.net.Uri.parse("content://com.android.shell/bugreports/clipboard");
|
||||
|
||||
ClipData clipData = new ClipData(
|
||||
null,
|
||||
new String[]{mimeType},
|
||||
new ClipData.Item(uri)
|
||||
);
|
||||
manager.setPrimaryClip(clipData);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Ln.e("Failed to set image clipboard", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setText(CharSequence text) {
|
||||
ClipData clipData = ClipData.newPlainText(null, text);
|
||||
manager.setPrimaryClip(clipData);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue