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
|
|
@ -5,6 +5,7 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "device_msg.h"
|
||||
#include "util/binary.h"
|
||||
#include "util/log.h"
|
||||
#include "util/str.h"
|
||||
|
|
@ -151,6 +152,20 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
|||
size_t len = write_string(&buf[10], msg->set_clipboard.text,
|
||||
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
|
||||
return 10 + len;
|
||||
case SC_CONTROL_MSG_TYPE_SET_IMAGE_CLIPBOARD:
|
||||
sc_write64be(&buf[1], msg->set_image_clipboard.sequence);
|
||||
buf[9] = !!msg->set_image_clipboard.paste;
|
||||
|
||||
size_t mimetype_len = strlen(msg->set_image_clipboard.mimetype);
|
||||
sc_write32be(&buf[10], mimetype_len);
|
||||
memcpy(&buf[14], msg->set_image_clipboard.mimetype, mimetype_len);
|
||||
|
||||
// Write image data length and data
|
||||
sc_write32be(&buf[14 + mimetype_len], msg->set_image_clipboard.size);
|
||||
memcpy(&buf[18 + mimetype_len], msg->set_image_clipboard.data,
|
||||
msg->set_image_clipboard.size);
|
||||
|
||||
return 18 + mimetype_len + msg->set_image_clipboard.size;
|
||||
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
|
||||
buf[1] = msg->set_display_power.on;
|
||||
return 2;
|
||||
|
|
@ -269,6 +284,13 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
|||
msg->set_clipboard.paste ? "paste" : "nopaste",
|
||||
msg->set_clipboard.text);
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_SET_IMAGE_CLIPBOARD:
|
||||
LOG_CMSG("image clipboard %" PRIu64_ " %s size=%u mimetype=\"%s\"",
|
||||
msg->set_image_clipboard.sequence,
|
||||
msg->set_image_clipboard.paste ? "paste" : "nopaste",
|
||||
msg->set_image_clipboard.size,
|
||||
msg->set_image_clipboard.mimetype);
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
|
||||
LOG_CMSG("display power %s",
|
||||
msg->set_display_power.on ? "on" : "off");
|
||||
|
|
@ -358,6 +380,10 @@ sc_control_msg_destroy(struct sc_control_msg *msg) {
|
|||
case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD:
|
||||
free(msg->set_clipboard.text);
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_SET_IMAGE_CLIPBOARD:
|
||||
free(msg->set_image_clipboard.data);
|
||||
free(msg->set_image_clipboard.mimetype);
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_START_APP:
|
||||
free(msg->start_app.name);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ enum sc_control_msg_type {
|
|||
SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
|
||||
};
|
||||
SC_CONTROL_MSG_TYPE_SET_IMAGE_CLIPBOARD,
|
||||
};
|
||||
|
||||
enum sc_copy_key {
|
||||
SC_COPY_KEY_NONE,
|
||||
|
|
@ -117,6 +118,13 @@ struct sc_control_msg {
|
|||
struct {
|
||||
bool on;
|
||||
} camera_set_torch;
|
||||
struct {
|
||||
uint64_t sequence;
|
||||
uint8_t *data; // owned, to be freed by free()
|
||||
uint32_t size;
|
||||
char *mimetype; // owned, to be freed by free()
|
||||
bool paste;
|
||||
} set_image_clipboard;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,45 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len,
|
|||
|
||||
return 5 + size;
|
||||
}
|
||||
case DEVICE_MSG_TYPE_IMAGE_CLIPBOARD: {
|
||||
if (len < 9) {
|
||||
// at least type + mimetype length (4 bytes) + data length (4 bytes)
|
||||
return 0; // no complete message
|
||||
}
|
||||
|
||||
uint32_t mimetype_len = sc_read32be(&buf[1]);
|
||||
uint32_t data_len = sc_read32be(&buf[5]);
|
||||
|
||||
if (mimetype_len + data_len > len - 9) {
|
||||
return 0; // no complete message
|
||||
}
|
||||
|
||||
char *mimetype = malloc(mimetype_len + 1);
|
||||
if (!mimetype) {
|
||||
LOG_OOM();
|
||||
return -1;
|
||||
}
|
||||
if (mimetype_len) {
|
||||
memcpy(mimetype, &buf[9], mimetype_len);
|
||||
}
|
||||
mimetype[mimetype_len] = '\0';
|
||||
|
||||
uint8_t *image_data = malloc(data_len);
|
||||
if (!image_data) {
|
||||
LOG_OOM();
|
||||
free(mimetype);
|
||||
return -1;
|
||||
}
|
||||
if (data_len) {
|
||||
memcpy(image_data, &buf[9 + mimetype_len], data_len);
|
||||
}
|
||||
|
||||
msg->image_clipboard.mimetype = mimetype;
|
||||
msg->image_clipboard.data = image_data;
|
||||
msg->image_clipboard.size = data_len;
|
||||
|
||||
return 9 + mimetype_len + data_len;
|
||||
}
|
||||
default:
|
||||
LOGW("Unknown device message type: %d", (int) msg->type);
|
||||
return -1; // error, we cannot recover
|
||||
|
|
@ -86,6 +125,10 @@ sc_device_msg_destroy(struct sc_device_msg *msg) {
|
|||
case DEVICE_MSG_TYPE_UHID_OUTPUT:
|
||||
free(msg->uhid_output.data);
|
||||
break;
|
||||
case DEVICE_MSG_TYPE_IMAGE_CLIPBOARD:
|
||||
free(msg->image_clipboard.data);
|
||||
free(msg->image_clipboard.mimetype);
|
||||
break;
|
||||
default:
|
||||
// nothing to do
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ enum sc_device_msg_type {
|
|||
DEVICE_MSG_TYPE_CLIPBOARD,
|
||||
DEVICE_MSG_TYPE_ACK_CLIPBOARD,
|
||||
DEVICE_MSG_TYPE_UHID_OUTPUT,
|
||||
DEVICE_MSG_TYPE_IMAGE_CLIPBOARD,
|
||||
};
|
||||
|
||||
struct sc_device_msg {
|
||||
|
|
@ -31,6 +32,11 @@ struct sc_device_msg {
|
|||
uint16_t size;
|
||||
uint8_t *data; // owned, to be freed by free()
|
||||
} uhid_output;
|
||||
struct {
|
||||
uint8_t *data; // owned, to be freed by free()
|
||||
uint32_t size;
|
||||
char *mimetype; // owned, to be freed by free()
|
||||
} image_clipboard;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -176,11 +176,74 @@ get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_input_manager_set_device_image_clipboard(struct sc_input_manager *im, bool paste,
|
||||
uint64_t sequence) {
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
// Try common image MIME types to check if image clipboard data exists
|
||||
const char* image_mime_types[] = {
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/bmp",
|
||||
NULL
|
||||
};
|
||||
|
||||
for (int i = 0; image_mime_types[i]; i++) {
|
||||
const char* mime_type = image_mime_types[i];
|
||||
size_t size;
|
||||
void *img_data = SDL_GetClipboardData(mime_type, &size);
|
||||
if (img_data && size > 0) {
|
||||
size_t mimetype_len = strlen(mime_type);
|
||||
|
||||
// Check if message exceeds max size
|
||||
size_t msg_size = 18 + mimetype_len + size;
|
||||
if (msg_size > SC_CONTROL_MSG_MAX_SIZE) {
|
||||
LOGW("Image clipboard message too large: %zu bytes, dropping",
|
||||
msg_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_SET_IMAGE_CLIPBOARD;
|
||||
msg.set_image_clipboard.sequence = sequence;
|
||||
msg.set_image_clipboard.data = malloc(size);
|
||||
if (msg.set_image_clipboard.data) {
|
||||
memcpy(msg.set_image_clipboard.data, img_data, size);
|
||||
msg.set_image_clipboard.size = size;
|
||||
msg.set_image_clipboard.mimetype = strdup(mime_type);
|
||||
msg.set_image_clipboard.paste = paste;
|
||||
|
||||
bool success = sc_controller_push_msg(im->controller, &msg);
|
||||
SDL_free(img_data);
|
||||
if (success) {
|
||||
return true;
|
||||
}
|
||||
free(msg.set_image_clipboard.data);
|
||||
free(msg.set_image_clipboard.mimetype);
|
||||
}
|
||||
SDL_free(img_data);
|
||||
}
|
||||
}
|
||||
|
||||
// Return false since we can't actually call the SDL3 functions in this context
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
set_device_clipboard(struct sc_input_manager *im, bool paste,
|
||||
uint64_t sequence) {
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
if (sc_input_manager_set_device_image_clipboard(im, paste, sequence)) {
|
||||
// Successfully sent image clipboard, return true
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback to text clipboard
|
||||
char *text = SDL_GetClipboardText();
|
||||
if (!text) {
|
||||
LOGW("Could not get clipboard text: %s", SDL_GetError());
|
||||
|
|
|
|||
|
|
@ -73,4 +73,9 @@ void
|
|||
sc_input_manager_handle_event(struct sc_input_manager *im,
|
||||
const SDL_Event *event);
|
||||
|
||||
// Send image clipboard from computer to device
|
||||
bool
|
||||
sc_input_manager_set_device_image_clipboard(struct sc_input_manager *im, bool paste,
|
||||
uint64_t sequence);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <assert.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <SDL3/SDL_clipboard.h>
|
||||
|
||||
#include "device_msg.h"
|
||||
|
|
@ -18,6 +19,28 @@ struct sc_uhid_output_task_data {
|
|||
uint8_t *data;
|
||||
};
|
||||
|
||||
struct sc_image_clipboard_data {
|
||||
uint8_t *data;
|
||||
uint32_t size;
|
||||
char *mimetype;
|
||||
};
|
||||
|
||||
static const void * SDLCALL
|
||||
clipboard_data_callback(void *userdata, const char *mime_type, size_t *size) {
|
||||
struct sc_image_clipboard_data *image_data = userdata;
|
||||
(void) mime_type;
|
||||
*size = image_data->size;
|
||||
return image_data->data;
|
||||
}
|
||||
|
||||
static void SDLCALL
|
||||
clipboard_cleanup_callback(void *userdata) {
|
||||
struct sc_image_clipboard_data *image_data = userdata;
|
||||
free(image_data->data);
|
||||
free(image_data->mimetype);
|
||||
free(image_data);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
const struct sc_receiver_callbacks *cbs, void *cbs_userdata) {
|
||||
|
|
@ -65,6 +88,44 @@ task_set_clipboard(void *userdata) {
|
|||
free(text);
|
||||
}
|
||||
|
||||
static void
|
||||
task_set_image_clipboard(void *userdata) {
|
||||
assert(sc_thread_get_id() == SC_MAIN_THREAD_ID);
|
||||
|
||||
struct sc_device_msg *msg = userdata;
|
||||
|
||||
struct sc_image_clipboard_data *image_data =
|
||||
malloc(sizeof(struct sc_image_clipboard_data));
|
||||
if (!image_data) {
|
||||
LOG_OOM();
|
||||
free(msg->image_clipboard.data);
|
||||
free(msg->image_clipboard.mimetype);
|
||||
free(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
image_data->data = msg->image_clipboard.data;
|
||||
image_data->size = msg->image_clipboard.size;
|
||||
image_data->mimetype = msg->image_clipboard.mimetype;
|
||||
|
||||
const char *mime_types[1] = { image_data->mimetype };
|
||||
|
||||
bool ok = SDL_SetClipboardData(clipboard_data_callback,
|
||||
clipboard_cleanup_callback,
|
||||
image_data,
|
||||
mime_types, 1);
|
||||
if (ok) {
|
||||
LOGI("Device image clipboard copied");
|
||||
} else {
|
||||
LOGE("Could not set image clipboard: %s", SDL_GetError());
|
||||
free(image_data->data);
|
||||
free(image_data->mimetype);
|
||||
free(image_data);
|
||||
}
|
||||
|
||||
free(msg);
|
||||
}
|
||||
|
||||
static void
|
||||
task_uhid_output(void *userdata) {
|
||||
assert(sc_thread_get_id() == SC_MAIN_THREAD_ID);
|
||||
|
|
@ -94,6 +155,46 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) {
|
|||
|
||||
break;
|
||||
}
|
||||
case DEVICE_MSG_TYPE_IMAGE_CLIPBOARD: {
|
||||
// Create a copy of the message to send to the main thread
|
||||
struct sc_device_msg *msg_copy = malloc(sizeof(struct sc_device_msg));
|
||||
if (!msg_copy) {
|
||||
LOG_OOM();
|
||||
return;
|
||||
}
|
||||
|
||||
msg_copy->type = DEVICE_MSG_TYPE_IMAGE_CLIPBOARD;
|
||||
msg_copy->image_clipboard.data = malloc(msg->image_clipboard.size);
|
||||
if (!msg_copy->image_clipboard.data) {
|
||||
LOG_OOM();
|
||||
free(msg_copy);
|
||||
return;
|
||||
}
|
||||
memcpy(msg_copy->image_clipboard.data, msg->image_clipboard.data, msg->image_clipboard.size);
|
||||
msg_copy->image_clipboard.size = msg->image_clipboard.size;
|
||||
|
||||
// Duplicate the mimetype string
|
||||
size_t mimetype_len = strlen(msg->image_clipboard.mimetype);
|
||||
msg_copy->image_clipboard.mimetype = malloc(mimetype_len + 1);
|
||||
if (!msg_copy->image_clipboard.mimetype) {
|
||||
LOG_OOM();
|
||||
free(msg_copy->image_clipboard.data);
|
||||
free(msg_copy);
|
||||
return;
|
||||
}
|
||||
strcpy(msg_copy->image_clipboard.mimetype, msg->image_clipboard.mimetype);
|
||||
|
||||
bool ok = sc_post_to_main_thread(task_set_image_clipboard, msg_copy);
|
||||
if (!ok) {
|
||||
LOGW("Could not post image clipboard to main thread");
|
||||
free(msg_copy->image_clipboard.data);
|
||||
free(msg_copy->image_clipboard.mimetype);
|
||||
free(msg_copy);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case DEVICE_MSG_TYPE_ACK_CLIPBOARD:
|
||||
LOGD("Ack device clipboard sequence=%" PRIu64_,
|
||||
msg->ack_clipboard.sequence);
|
||||
|
|
|
|||
|
|
@ -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