Compare commits
27 Commits
feature
...
fakecontex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7596966fe7 | ||
|
|
6a37a564e5 | ||
|
|
b097f04459 | ||
|
|
cbabaa223e | ||
|
|
c91253105e | ||
|
|
4332b53dd5 | ||
|
|
2e9c364ef3 | ||
|
|
9b286ec8a7 | ||
|
|
8c5c55f9e1 | ||
|
|
0afef0c634 | ||
|
|
07806ba915 | ||
|
|
a52053421a | ||
|
|
a9b2697f3e | ||
|
|
b53d2c66e0 | ||
|
|
6cccf3ab2a | ||
|
|
52f85fd6f1 | ||
|
|
91c69ad95c | ||
|
|
75d7c01a0c | ||
|
|
74d32e612d | ||
|
|
bdba554118 | ||
|
|
234ad7ee78 | ||
|
|
8cbbcc939f | ||
|
|
b22810b17c | ||
|
|
4315be1648 | ||
|
|
74e3f8b253 | ||
|
|
059ec45f82 | ||
|
|
e6cd42355b |
@@ -37,6 +37,7 @@ src = [
|
|||||||
'src/util/net_intr.c',
|
'src/util/net_intr.c',
|
||||||
'src/util/process.c',
|
'src/util/process.c',
|
||||||
'src/util/process_intr.c',
|
'src/util/process_intr.c',
|
||||||
|
'src/util/rand.c',
|
||||||
'src/util/strbuf.c',
|
'src/util/strbuf.c',
|
||||||
'src/util/str.c',
|
'src/util/str.c',
|
||||||
'src/util/term.c',
|
'src/util/term.c',
|
||||||
@@ -170,6 +171,8 @@ check_functions = [
|
|||||||
'strdup',
|
'strdup',
|
||||||
'asprintf',
|
'asprintf',
|
||||||
'vasprintf',
|
'vasprintf',
|
||||||
|
'nrand48',
|
||||||
|
'jrand48',
|
||||||
]
|
]
|
||||||
|
|
||||||
foreach f : check_functions
|
foreach f : check_functions
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
#include "util/net_intr.h"
|
#include "util/net_intr.h"
|
||||||
#include "util/process_intr.h"
|
#include "util/process_intr.h"
|
||||||
|
|
||||||
#define SC_SOCKET_NAME "scrcpy"
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
|
listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
|
||||||
return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1);
|
return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1);
|
||||||
@@ -17,10 +15,11 @@ listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
|
|||||||
static bool
|
static bool
|
||||||
enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
|
enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
|
||||||
struct sc_intr *intr, const char *serial,
|
struct sc_intr *intr, const char *serial,
|
||||||
|
const char *device_socket_name,
|
||||||
struct sc_port_range port_range) {
|
struct sc_port_range port_range) {
|
||||||
uint16_t port = port_range.first;
|
uint16_t port = port_range.first;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (!sc_adb_reverse(intr, serial, SC_SOCKET_NAME, port,
|
if (!sc_adb_reverse(intr, serial, device_socket_name, port,
|
||||||
SC_ADB_NO_STDOUT)) {
|
SC_ADB_NO_STDOUT)) {
|
||||||
// the command itself failed, it will fail on any port
|
// the command itself failed, it will fail on any port
|
||||||
return false;
|
return false;
|
||||||
@@ -52,7 +51,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// failure, disable tunnel and try another port
|
// failure, disable tunnel and try another port
|
||||||
if (!sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME,
|
if (!sc_adb_reverse_remove(intr, serial, device_socket_name,
|
||||||
SC_ADB_NO_STDOUT)) {
|
SC_ADB_NO_STDOUT)) {
|
||||||
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
|
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
|
||||||
}
|
}
|
||||||
@@ -78,12 +77,13 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
|
|||||||
static bool
|
static bool
|
||||||
enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel,
|
enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel,
|
||||||
struct sc_intr *intr, const char *serial,
|
struct sc_intr *intr, const char *serial,
|
||||||
|
const char *device_socket_name,
|
||||||
struct sc_port_range port_range) {
|
struct sc_port_range port_range) {
|
||||||
tunnel->forward = true;
|
tunnel->forward = true;
|
||||||
|
|
||||||
uint16_t port = port_range.first;
|
uint16_t port = port_range.first;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (sc_adb_forward(intr, serial, port, SC_SOCKET_NAME,
|
if (sc_adb_forward(intr, serial, port, device_socket_name,
|
||||||
SC_ADB_NO_STDOUT)) {
|
SC_ADB_NO_STDOUT)) {
|
||||||
// success
|
// success
|
||||||
tunnel->local_port = port;
|
tunnel->local_port = port;
|
||||||
@@ -123,13 +123,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) {
|
|||||||
|
|
||||||
bool
|
bool
|
||||||
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||||
const char *serial, struct sc_port_range port_range,
|
const char *serial, const char *device_socket_name,
|
||||||
bool force_adb_forward) {
|
struct sc_port_range port_range, bool force_adb_forward) {
|
||||||
assert(!tunnel->enabled);
|
assert(!tunnel->enabled);
|
||||||
|
|
||||||
if (!force_adb_forward) {
|
if (!force_adb_forward) {
|
||||||
// Attempt to use "adb reverse"
|
// Attempt to use "adb reverse"
|
||||||
if (enable_tunnel_reverse_any_port(tunnel, intr, serial, port_range)) {
|
if (enable_tunnel_reverse_any_port(tunnel, intr, serial,
|
||||||
|
device_socket_name, port_range)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,12 +140,13 @@ sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
|||||||
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
||||||
}
|
}
|
||||||
|
|
||||||
return enable_tunnel_forward_any_port(tunnel, intr, serial, port_range);
|
return enable_tunnel_forward_any_port(tunnel, intr, serial,
|
||||||
|
device_socket_name, port_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||||
const char *serial) {
|
const char *serial, const char *device_socket_name) {
|
||||||
assert(tunnel->enabled);
|
assert(tunnel->enabled);
|
||||||
|
|
||||||
bool ret;
|
bool ret;
|
||||||
@@ -152,7 +154,7 @@ sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
|||||||
ret = sc_adb_forward_remove(intr, serial, tunnel->local_port,
|
ret = sc_adb_forward_remove(intr, serial, tunnel->local_port,
|
||||||
SC_ADB_NO_STDOUT);
|
SC_ADB_NO_STDOUT);
|
||||||
} else {
|
} else {
|
||||||
ret = sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME,
|
ret = sc_adb_reverse_remove(intr, serial, device_socket_name,
|
||||||
SC_ADB_NO_STDOUT);
|
SC_ADB_NO_STDOUT);
|
||||||
|
|
||||||
assert(tunnel->server_socket != SC_SOCKET_NONE);
|
assert(tunnel->server_socket != SC_SOCKET_NONE);
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel);
|
|||||||
*/
|
*/
|
||||||
bool
|
bool
|
||||||
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||||
const char *serial, struct sc_port_range port_range,
|
const char *serial, const char *device_socket_name,
|
||||||
bool force_adb_forward);
|
struct sc_port_range port_range, bool force_adb_forward);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the tunnel
|
* Close the tunnel
|
||||||
*/
|
*/
|
||||||
bool
|
bool
|
||||||
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||||
const char *serial);
|
const char *serial, const char *device_socket_name);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -51,3 +51,47 @@ int vasprintf(char **strp, const char *fmt, va_list ap) {
|
|||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !defined(HAVE_NRAND48) || !defined(HAVE_JRAND48)
|
||||||
|
#define SC_RAND48_MASK UINT64_C(0xFFFFFFFFFFFF) // 48 bits
|
||||||
|
#define SC_RAND48_A UINT64_C(0x5DEECE66D)
|
||||||
|
#define SC_RAND48_C 0xB
|
||||||
|
static inline uint64_t rand_iter48(uint64_t x) {
|
||||||
|
assert((x & ~SC_RAND48_MASK) == 0);
|
||||||
|
return (x * SC_RAND48_A + SC_RAND48_C) & SC_RAND48_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t rand_iter48_xsubi(unsigned short xsubi[3]) {
|
||||||
|
uint64_t x = ((uint64_t) xsubi[0] << 32)
|
||||||
|
| ((uint64_t) xsubi[1] << 16)
|
||||||
|
| xsubi[2];
|
||||||
|
|
||||||
|
x = rand_iter48(x);
|
||||||
|
|
||||||
|
xsubi[0] = (x >> 32) & 0XFFFF;
|
||||||
|
xsubi[1] = (x >> 16) & 0XFFFF;
|
||||||
|
xsubi[2] = x & 0XFFFF;
|
||||||
|
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef HAVE_NRAND48
|
||||||
|
long nrand48(unsigned short xsubi[3]) {
|
||||||
|
// range [0, 2^31)
|
||||||
|
return rand_iter48_xsubi(xsubi) >> 17;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef HAVE_JRAND48
|
||||||
|
long jrand48(unsigned short xsubi[3]) {
|
||||||
|
// range [-2^31, 2^31)
|
||||||
|
union {
|
||||||
|
uint32_t u;
|
||||||
|
int32_t i;
|
||||||
|
} v;
|
||||||
|
v.u = rand_iter48_xsubi(xsubi) >> 16;
|
||||||
|
return v.i;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -59,4 +59,12 @@ int asprintf(char **strp, const char *fmt, ...);
|
|||||||
int vasprintf(char **strp, const char *fmt, va_list ap);
|
int vasprintf(char **strp, const char *fmt, va_list ap);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef HAVE_NRAND48
|
||||||
|
long nrand48(unsigned short xsubi[3]);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef HAVE_JRAND48
|
||||||
|
long jrand48(unsigned short xsubi[3]);
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -117,8 +117,9 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
|
|||||||
uint16_t pressure =
|
uint16_t pressure =
|
||||||
sc_float_to_u16fp(msg->inject_touch_event.pressure);
|
sc_float_to_u16fp(msg->inject_touch_event.pressure);
|
||||||
sc_write16be(&buf[22], pressure);
|
sc_write16be(&buf[22], pressure);
|
||||||
sc_write32be(&buf[24], msg->inject_touch_event.buttons);
|
sc_write32be(&buf[24], msg->inject_touch_event.action_button);
|
||||||
return 28;
|
sc_write32be(&buf[28], msg->inject_touch_event.buttons);
|
||||||
|
return 32;
|
||||||
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
|
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
|
||||||
write_position(&buf[1], &msg->inject_scroll_event.position);
|
write_position(&buf[1], &msg->inject_scroll_event.position);
|
||||||
int16_t hscroll =
|
int16_t hscroll =
|
||||||
@@ -179,22 +180,25 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
|||||||
if (pointer_name) {
|
if (pointer_name) {
|
||||||
// string pointer id
|
// string pointer id
|
||||||
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
|
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
|
||||||
" pressure=%f buttons=%06lx",
|
" pressure=%f action_button=%06lx buttons=%06lx",
|
||||||
pointer_name,
|
pointer_name,
|
||||||
MOTIONEVENT_ACTION_LABEL(action),
|
MOTIONEVENT_ACTION_LABEL(action),
|
||||||
msg->inject_touch_event.position.point.x,
|
msg->inject_touch_event.position.point.x,
|
||||||
msg->inject_touch_event.position.point.y,
|
msg->inject_touch_event.position.point.y,
|
||||||
msg->inject_touch_event.pressure,
|
msg->inject_touch_event.pressure,
|
||||||
|
(long) msg->inject_touch_event.action_button,
|
||||||
(long) msg->inject_touch_event.buttons);
|
(long) msg->inject_touch_event.buttons);
|
||||||
} else {
|
} else {
|
||||||
// numeric pointer id
|
// numeric pointer id
|
||||||
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
|
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
|
||||||
PRIi32 " pressure=%f buttons=%06lx",
|
PRIi32 " pressure=%f action_button=%06lx"
|
||||||
|
" buttons=%06lx",
|
||||||
id,
|
id,
|
||||||
MOTIONEVENT_ACTION_LABEL(action),
|
MOTIONEVENT_ACTION_LABEL(action),
|
||||||
msg->inject_touch_event.position.point.x,
|
msg->inject_touch_event.position.point.x,
|
||||||
msg->inject_touch_event.position.point.y,
|
msg->inject_touch_event.position.point.y,
|
||||||
msg->inject_touch_event.pressure,
|
msg->inject_touch_event.pressure,
|
||||||
|
(long) msg->inject_touch_event.action_button,
|
||||||
(long) msg->inject_touch_event.buttons);
|
(long) msg->inject_touch_event.buttons);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ struct sc_control_msg {
|
|||||||
} inject_text;
|
} inject_text;
|
||||||
struct {
|
struct {
|
||||||
enum android_motionevent_action action;
|
enum android_motionevent_action action;
|
||||||
|
enum android_motionevent_buttons action_button;
|
||||||
enum android_motionevent_buttons buttons;
|
enum android_motionevent_buttons buttons;
|
||||||
uint64_t pointer_id;
|
uint64_t pointer_id;
|
||||||
struct sc_position position;
|
struct sc_position position;
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ simulate_virtual_finger(struct sc_input_manager *im,
|
|||||||
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
|
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
|
||||||
: POINTER_ID_VIRTUAL_FINGER;
|
: POINTER_ID_VIRTUAL_FINGER;
|
||||||
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
|
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
|
||||||
|
msg.inject_touch_event.action_button = 0;
|
||||||
msg.inject_touch_event.buttons = 0;
|
msg.inject_touch_event.buttons = 0;
|
||||||
|
|
||||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
|
|||||||
.pointer_id = event->pointer_id,
|
.pointer_id = event->pointer_id,
|
||||||
.position = event->position,
|
.position = event->position,
|
||||||
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
|
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
|
||||||
|
.action_button = convert_mouse_buttons(event->button),
|
||||||
.buttons = convert_mouse_buttons(event->buttons_state),
|
.buttons = convert_mouse_buttons(event->buttons_state),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "util/acksync.h"
|
#include "util/acksync.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
#include "util/net.h"
|
#include "util/net.h"
|
||||||
|
#include "util/rand.h"
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
# include "v4l2_sink.h"
|
# include "v4l2_sink.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -265,6 +266,14 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
|
|||||||
// event
|
// event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint32_t
|
||||||
|
scrcpy_generate_uid() {
|
||||||
|
struct sc_rand rand;
|
||||||
|
sc_rand_init(&rand);
|
||||||
|
// Only use 31 bits to avoid issues with signed values on the Java-side
|
||||||
|
return sc_rand_u32(&rand) & 0x7FFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
enum scrcpy_exit_code
|
enum scrcpy_exit_code
|
||||||
scrcpy(struct scrcpy_options *options) {
|
scrcpy(struct scrcpy_options *options) {
|
||||||
static struct scrcpy scrcpy;
|
static struct scrcpy scrcpy;
|
||||||
@@ -298,7 +307,10 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
|
|
||||||
struct sc_acksync *acksync = NULL;
|
struct sc_acksync *acksync = NULL;
|
||||||
|
|
||||||
|
uint32_t uid = scrcpy_generate_uid();
|
||||||
|
|
||||||
struct sc_server_params params = {
|
struct sc_server_params params = {
|
||||||
|
.uid = uid,
|
||||||
.req_serial = options->serial,
|
.req_serial = options->serial,
|
||||||
.select_usb = options->select_usb,
|
.select_usb = options->select_usb,
|
||||||
.select_tcpip = options->select_tcpip,
|
.select_tcpip = options->select_tcpip,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
|
#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
|
||||||
|
|
||||||
#define SC_ADB_PORT_DEFAULT 5555
|
#define SC_ADB_PORT_DEFAULT 5555
|
||||||
|
#define SC_SOCKET_NAME_PREFIX "scrcpy_"
|
||||||
|
|
||||||
static char *
|
static char *
|
||||||
get_server_path(void) {
|
get_server_path(void) {
|
||||||
@@ -197,6 +198,7 @@ execute_server(struct sc_server *server,
|
|||||||
cmd[count++] = p; \
|
cmd[count++] = p; \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ADD_PARAM("uid=%08x", params->uid);
|
||||||
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
|
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
|
||||||
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
|
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
|
||||||
|
|
||||||
@@ -364,6 +366,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
|
|||||||
}
|
}
|
||||||
|
|
||||||
server->serial = NULL;
|
server->serial = NULL;
|
||||||
|
server->device_socket_name = NULL;
|
||||||
server->stopped = false;
|
server->stopped = false;
|
||||||
|
|
||||||
server->video_socket = SC_SOCKET_NONE;
|
server->video_socket = SC_SOCKET_NONE;
|
||||||
@@ -463,7 +466,8 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we don't need the adb tunnel anymore
|
// we don't need the adb tunnel anymore
|
||||||
sc_adb_tunnel_close(tunnel, &server->intr, serial);
|
sc_adb_tunnel_close(tunnel, &server->intr, serial,
|
||||||
|
server->device_socket_name);
|
||||||
|
|
||||||
// The sockets will be closed on stop if device_read_info() fails
|
// The sockets will be closed on stop if device_read_info() fails
|
||||||
bool ok = device_read_info(&server->intr, video_socket, info);
|
bool ok = device_read_info(&server->intr, video_socket, info);
|
||||||
@@ -494,7 +498,8 @@ fail:
|
|||||||
|
|
||||||
if (tunnel->enabled) {
|
if (tunnel->enabled) {
|
||||||
// Always leave this function with tunnel disabled
|
// Always leave this function with tunnel disabled
|
||||||
sc_adb_tunnel_close(tunnel, &server->intr, serial);
|
sc_adb_tunnel_close(tunnel, &server->intr, serial,
|
||||||
|
server->device_socket_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -764,13 +769,23 @@ run_server(void *data) {
|
|||||||
assert(serial);
|
assert(serial);
|
||||||
LOGD("Device serial: %s", serial);
|
LOGD("Device serial: %s", serial);
|
||||||
|
|
||||||
|
int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x",
|
||||||
|
params->uid);
|
||||||
|
if (r == -1) {
|
||||||
|
LOG_OOM();
|
||||||
|
goto error_connection_failed;
|
||||||
|
}
|
||||||
|
assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8);
|
||||||
|
assert(server->device_socket_name);
|
||||||
|
|
||||||
ok = push_server(&server->intr, serial);
|
ok = push_server(&server->intr, serial);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
goto error_connection_failed;
|
goto error_connection_failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial,
|
ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial,
|
||||||
params->port_range, params->force_adb_forward);
|
server->device_socket_name, params->port_range,
|
||||||
|
params->force_adb_forward);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
goto error_connection_failed;
|
goto error_connection_failed;
|
||||||
}
|
}
|
||||||
@@ -778,7 +793,8 @@ run_server(void *data) {
|
|||||||
// server will connect to our server socket
|
// server will connect to our server socket
|
||||||
sc_pid pid = execute_server(server, params);
|
sc_pid pid = execute_server(server, params);
|
||||||
if (pid == SC_PROCESS_NONE) {
|
if (pid == SC_PROCESS_NONE) {
|
||||||
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial);
|
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial,
|
||||||
|
server->device_socket_name);
|
||||||
goto error_connection_failed;
|
goto error_connection_failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -790,7 +806,8 @@ run_server(void *data) {
|
|||||||
if (!ok) {
|
if (!ok) {
|
||||||
sc_process_terminate(pid);
|
sc_process_terminate(pid);
|
||||||
sc_process_wait(pid, true); // ignore exit code
|
sc_process_wait(pid, true); // ignore exit code
|
||||||
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial);
|
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial,
|
||||||
|
server->device_socket_name);
|
||||||
goto error_connection_failed;
|
goto error_connection_failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,6 +901,7 @@ sc_server_destroy(struct sc_server *server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
free(server->serial);
|
free(server->serial);
|
||||||
|
free(server->device_socket_name);
|
||||||
sc_server_params_destroy(&server->params);
|
sc_server_params_destroy(&server->params);
|
||||||
sc_intr_destroy(&server->intr);
|
sc_intr_destroy(&server->intr);
|
||||||
sc_cond_destroy(&server->cond_stopped);
|
sc_cond_destroy(&server->cond_stopped);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ struct sc_server_info {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct sc_server_params {
|
struct sc_server_params {
|
||||||
|
uint32_t uid;
|
||||||
const char *req_serial;
|
const char *req_serial;
|
||||||
enum sc_log_level log_level;
|
enum sc_log_level log_level;
|
||||||
const char *crop;
|
const char *crop;
|
||||||
@@ -54,6 +55,7 @@ struct sc_server {
|
|||||||
// The internal allocated strings are copies owned by the server
|
// The internal allocated strings are copies owned by the server
|
||||||
struct sc_server_params params;
|
struct sc_server_params params;
|
||||||
char *serial;
|
char *serial;
|
||||||
|
char *device_socket_name;
|
||||||
|
|
||||||
sc_thread thread;
|
sc_thread thread;
|
||||||
struct sc_server_info info; // initialized once connected
|
struct sc_server_info info; // initialized once connected
|
||||||
|
|||||||
24
app/src/util/rand.c
Normal file
24
app/src/util/rand.c
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#include "rand.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "tick.h"
|
||||||
|
|
||||||
|
void sc_rand_init(struct sc_rand *rand) {
|
||||||
|
sc_tick seed = sc_tick_now(); // microsecond precision
|
||||||
|
rand->xsubi[0] = (seed >> 32) & 0xFFFF;
|
||||||
|
rand->xsubi[1] = (seed >> 16) & 0xFFFF;
|
||||||
|
rand->xsubi[2] = seed & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t sc_rand_u32(struct sc_rand *rand) {
|
||||||
|
// jrand returns a value in range [-2^31, 2^31]
|
||||||
|
// conversion from signed to unsigned is well-defined to wrap-around
|
||||||
|
return jrand48(rand->xsubi);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t sc_rand_u64(struct sc_rand *rand) {
|
||||||
|
uint32_t msb = sc_rand_u32(rand);
|
||||||
|
uint32_t lsb = sc_rand_u32(rand);
|
||||||
|
return ((uint64_t) msb << 32) | lsb;
|
||||||
|
}
|
||||||
16
app/src/util/rand.h
Normal file
16
app/src/util/rand.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#ifndef SC_RAND_H
|
||||||
|
#define SC_RAND_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <inttypes.h>
|
||||||
|
|
||||||
|
struct sc_rand {
|
||||||
|
unsigned short xsubi[3];
|
||||||
|
};
|
||||||
|
|
||||||
|
void sc_rand_init(struct sc_rand *rand);
|
||||||
|
uint32_t sc_rand_u32(struct sc_rand *rand);
|
||||||
|
uint64_t sc_rand_u64(struct sc_rand *rand);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -90,13 +90,14 @@ static void test_serialize_inject_touch_event(void) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
.pressure = 1.0f,
|
.pressure = 1.0f,
|
||||||
|
.action_button = AMOTION_EVENT_BUTTON_PRIMARY,
|
||||||
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
|
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
|
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||||
assert(size == 28);
|
assert(size == 32);
|
||||||
|
|
||||||
const unsigned char expected[] = {
|
const unsigned char expected[] = {
|
||||||
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
|
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
|
||||||
@@ -105,7 +106,8 @@ static void test_serialize_inject_touch_event(void) {
|
|||||||
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
|
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
|
||||||
0x04, 0x38, 0x07, 0x80, // 1080 1920
|
0x04, 0x38, 0x07, 0x80, // 1080 1920
|
||||||
0xff, 0xff, // pressure
|
0xff, 0xff, // pressure
|
||||||
0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
|
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button)
|
||||||
|
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons)
|
||||||
};
|
};
|
||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,18 +20,21 @@ BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
|
|||||||
|
|
||||||
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
||||||
CLASSES_DIR="$BUILD_DIR/classes"
|
CLASSES_DIR="$BUILD_DIR/classes"
|
||||||
|
GEN_DIR="$BUILD_DIR/gen"
|
||||||
SERVER_DIR=$(dirname "$0")
|
SERVER_DIR=$(dirname "$0")
|
||||||
SERVER_BINARY=scrcpy-server
|
SERVER_BINARY=scrcpy-server
|
||||||
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
|
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
|
||||||
|
LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar"
|
||||||
|
|
||||||
echo "Platform: android-$PLATFORM"
|
echo "Platform: android-$PLATFORM"
|
||||||
echo "Build-tools: $BUILD_TOOLS"
|
echo "Build-tools: $BUILD_TOOLS"
|
||||||
echo "Build dir: $BUILD_DIR"
|
echo "Build dir: $BUILD_DIR"
|
||||||
|
|
||||||
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
|
rm -rf "$CLASSES_DIR" "$GEN_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
|
||||||
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
|
mkdir -p "$CLASSES_DIR"
|
||||||
|
mkdir -p "$GEN_DIR/com/genymobile/scrcpy"
|
||||||
|
|
||||||
<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java"
|
<< EOF cat > "$GEN_DIR/com/genymobile/scrcpy/BuildConfig.java"
|
||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
public final class BuildConfig {
|
public final class BuildConfig {
|
||||||
@@ -42,13 +45,15 @@ EOF
|
|||||||
|
|
||||||
echo "Generating java from aidl..."
|
echo "Generating java from aidl..."
|
||||||
cd "$SERVER_DIR/src/main/aidl"
|
cd "$SERVER_DIR/src/main/aidl"
|
||||||
"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" android/view/IRotationWatcher.aidl
|
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
|
||||||
"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" \
|
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \
|
||||||
android/content/IOnPrimaryClipChangedListener.aidl
|
android/content/IOnPrimaryClipChangedListener.aidl
|
||||||
|
|
||||||
echo "Compiling java sources..."
|
echo "Compiling java sources..."
|
||||||
cd ../java
|
cd ../java
|
||||||
javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \
|
javac -bootclasspath "$ANDROID_JAR" \
|
||||||
|
-cp "$LAMBDA_JAR:$GEN_DIR" \
|
||||||
|
-d "$CLASSES_DIR" \
|
||||||
-source 1.8 -target 1.8 \
|
-source 1.8 -target 1.8 \
|
||||||
com/genymobile/scrcpy/*.java \
|
com/genymobile/scrcpy/*.java \
|
||||||
com/genymobile/scrcpy/wrappers/*.java
|
com/genymobile/scrcpy/wrappers/*.java
|
||||||
@@ -68,7 +73,7 @@ then
|
|||||||
echo "Archiving..."
|
echo "Archiving..."
|
||||||
cd "$BUILD_DIR"
|
cd "$BUILD_DIR"
|
||||||
jar cvf "$SERVER_BINARY" classes.dex
|
jar cvf "$SERVER_BINARY" classes.dex
|
||||||
rm -rf classes.dex classes
|
rm -rf classes.dex
|
||||||
else
|
else
|
||||||
# use d8
|
# use d8
|
||||||
"$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \
|
"$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \
|
||||||
@@ -80,7 +85,8 @@ else
|
|||||||
|
|
||||||
cd "$BUILD_DIR"
|
cd "$BUILD_DIR"
|
||||||
mv classes.zip "$SERVER_BINARY"
|
mv classes.zip "$SERVER_BINARY"
|
||||||
rm -rf classes
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
rm -rf "$GEN_DIR" "$CLASSES_DIR"
|
||||||
|
|
||||||
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"
|
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class CodecOption {
|
public class CodecOption {
|
||||||
private String key;
|
private final String key;
|
||||||
private Object value;
|
private final Object value;
|
||||||
|
|
||||||
public CodecOption(String key, Object value) {
|
public CodecOption(String key, Object value) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public final class ControlMessage {
|
|||||||
private int metaState; // KeyEvent.META_*
|
private int metaState; // KeyEvent.META_*
|
||||||
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
|
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
|
||||||
private int keycode; // KeyEvent.KEYCODE_*
|
private int keycode; // KeyEvent.KEYCODE_*
|
||||||
|
private int actionButton; // MotionEvent.BUTTON_*
|
||||||
private int buttons; // MotionEvent.BUTTON_*
|
private int buttons; // MotionEvent.BUTTON_*
|
||||||
private long pointerId;
|
private long pointerId;
|
||||||
private float pressure;
|
private float pressure;
|
||||||
@@ -60,13 +61,15 @@ public final class ControlMessage {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
|
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton,
|
||||||
|
int buttons) {
|
||||||
ControlMessage msg = new ControlMessage();
|
ControlMessage msg = new ControlMessage();
|
||||||
msg.type = TYPE_INJECT_TOUCH_EVENT;
|
msg.type = TYPE_INJECT_TOUCH_EVENT;
|
||||||
msg.action = action;
|
msg.action = action;
|
||||||
msg.pointerId = pointerId;
|
msg.pointerId = pointerId;
|
||||||
msg.pressure = pressure;
|
msg.pressure = pressure;
|
||||||
msg.position = position;
|
msg.position = position;
|
||||||
|
msg.actionButton = actionButton;
|
||||||
msg.buttons = buttons;
|
msg.buttons = buttons;
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
@@ -140,6 +143,10 @@ public final class ControlMessage {
|
|||||||
return keycode;
|
return keycode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getActionButton() {
|
||||||
|
return actionButton;
|
||||||
|
}
|
||||||
|
|
||||||
public int getButtons() {
|
public int getButtons() {
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
public class ControlMessageReader {
|
public class ControlMessageReader {
|
||||||
|
|
||||||
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
|
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
|
||||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31;
|
||||||
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||||
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
|
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
|
||||||
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||||
@@ -140,8 +140,9 @@ public class ControlMessageReader {
|
|||||||
long pointerId = buffer.getLong();
|
long pointerId = buffer.getLong();
|
||||||
Position position = readPosition(buffer);
|
Position position = readPosition(buffer);
|
||||||
float pressure = Binary.u16FixedPointToFloat(buffer.getShort());
|
float pressure = Binary.u16FixedPointToFloat(buffer.getShort());
|
||||||
|
int actionButton = buffer.getInt();
|
||||||
int buttons = buffer.getInt();
|
int buttons = buffer.getInt();
|
||||||
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
|
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControlMessage parseInjectScrollEvent() {
|
private ControlMessage parseInjectScrollEvent() {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||||
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
@@ -75,7 +77,7 @@ public class Controller {
|
|||||||
SystemClock.sleep(500);
|
SystemClock.sleep(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
handleEvent();
|
handleEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +101,7 @@ public class Controller {
|
|||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||||
if (device.supportsInputEvents()) {
|
if (device.supportsInputEvents()) {
|
||||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
|
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||||
@@ -179,7 +181,7 @@ public class Controller {
|
|||||||
return successCount;
|
return successCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
|
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
|
||||||
long now = SystemClock.uptimeMillis();
|
long now = SystemClock.uptimeMillis();
|
||||||
|
|
||||||
Point point = device.getPhysicalPoint(position);
|
Point point = device.getPhysicalPoint(position);
|
||||||
@@ -196,22 +198,23 @@ public class Controller {
|
|||||||
Pointer pointer = pointersState.get(pointerIndex);
|
Pointer pointer = pointersState.get(pointerIndex);
|
||||||
pointer.setPoint(point);
|
pointer.setPoint(point);
|
||||||
pointer.setPressure(pressure);
|
pointer.setPressure(pressure);
|
||||||
pointer.setUp(action == MotionEvent.ACTION_UP);
|
|
||||||
|
|
||||||
int source;
|
int source;
|
||||||
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
|
|
||||||
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
|
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
|
||||||
// real mouse event (forced by the client when --forward-on-click)
|
// real mouse event (forced by the client when --forward-on-click)
|
||||||
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
|
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
|
||||||
source = InputDevice.SOURCE_MOUSE;
|
source = InputDevice.SOURCE_MOUSE;
|
||||||
|
pointer.setUp(buttons == 0);
|
||||||
} else {
|
} else {
|
||||||
// POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device
|
// POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device
|
||||||
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER;
|
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||||
source = InputDevice.SOURCE_TOUCHSCREEN;
|
source = InputDevice.SOURCE_TOUCHSCREEN;
|
||||||
// Buttons must not be set for touch events
|
// Buttons must not be set for touch events
|
||||||
buttons = 0;
|
buttons = 0;
|
||||||
|
pointer.setUp(action == MotionEvent.ACTION_UP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
|
||||||
if (pointerCount == 1) {
|
if (pointerCount == 1) {
|
||||||
if (action == MotionEvent.ACTION_DOWN) {
|
if (action == MotionEvent.ACTION_DOWN) {
|
||||||
lastTouchDown = now;
|
lastTouchDown = now;
|
||||||
@@ -225,6 +228,62 @@ public class Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* If the input device is a mouse (on API >= 23):
|
||||||
|
* - the first button pressed must first generate ACTION_DOWN;
|
||||||
|
* - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS;
|
||||||
|
* - all button released (including the last one) must generate ACTION_BUTTON_RELEASE;
|
||||||
|
* - the last button released must in addition generate ACTION_UP.
|
||||||
|
*
|
||||||
|
* Otherwise, Chrome does not work properly: <https://github.com/Genymobile/scrcpy/issues/3635>
|
||||||
|
*/
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) {
|
||||||
|
if (action == MotionEvent.ACTION_DOWN) {
|
||||||
|
if (actionButton == buttons) {
|
||||||
|
// First button pressed: ACTION_DOWN
|
||||||
|
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
|
||||||
|
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||||
|
if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any button pressed: ACTION_BUTTON_PRESS
|
||||||
|
MotionEvent pressEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_PRESS, pointerCount, pointerProperties,
|
||||||
|
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||||
|
if (!InputManager.setActionButton(pressEvent, actionButton)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == MotionEvent.ACTION_UP) {
|
||||||
|
// Any button released: ACTION_BUTTON_RELEASE
|
||||||
|
MotionEvent releaseEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_RELEASE, pointerCount, pointerProperties,
|
||||||
|
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||||
|
if (!InputManager.setActionButton(releaseEvent, actionButton)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttons == 0) {
|
||||||
|
// Last button released: ACTION_UP
|
||||||
|
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
|
||||||
|
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||||
|
if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MotionEvent event = MotionEvent
|
MotionEvent event = MotionEvent
|
||||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
|
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
|
||||||
0);
|
0);
|
||||||
@@ -258,12 +317,9 @@ public class Controller {
|
|||||||
* Schedule a call to set power mode to off after a small delay.
|
* Schedule a call to set power mode to off after a small delay.
|
||||||
*/
|
*/
|
||||||
private static void schedulePowerModeOff() {
|
private static void schedulePowerModeOff() {
|
||||||
EXECUTOR.schedule(new Runnable() {
|
EXECUTOR.schedule(() -> {
|
||||||
@Override
|
Ln.i("Forcing screen off");
|
||||||
public void run() {
|
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
|
||||||
Ln.i("Forcing screen off");
|
|
||||||
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
|
|
||||||
}
|
|
||||||
}, 200, TimeUnit.MILLISECONDS);
|
}, 200, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public final class DesktopConnection implements Closeable {
|
|||||||
|
|
||||||
private static final int DEVICE_NAME_FIELD_LENGTH = 64;
|
private static final int DEVICE_NAME_FIELD_LENGTH = 64;
|
||||||
|
|
||||||
private static final String SOCKET_NAME = "scrcpy";
|
private static final String SOCKET_NAME_PREFIX = "scrcpy";
|
||||||
|
|
||||||
private final LocalSocket videoSocket;
|
private final LocalSocket videoSocket;
|
||||||
private final FileDescriptor videoFd;
|
private final FileDescriptor videoFd;
|
||||||
@@ -46,12 +46,22 @@ public final class DesktopConnection implements Closeable {
|
|||||||
return localSocket;
|
return localSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DesktopConnection open(boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
|
private static String getSocketName(int uid) {
|
||||||
|
if (uid == -1) {
|
||||||
|
// If no UID is set, use "scrcpy" to simplify using scrcpy-server alone
|
||||||
|
return SOCKET_NAME_PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SOCKET_NAME_PREFIX + String.format("_%08x", uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DesktopConnection open(int uid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
|
||||||
|
String socketName = getSocketName(uid);
|
||||||
|
|
||||||
LocalSocket videoSocket;
|
LocalSocket videoSocket;
|
||||||
LocalSocket controlSocket = null;
|
LocalSocket controlSocket = null;
|
||||||
if (tunnelForward) {
|
if (tunnelForward) {
|
||||||
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
|
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
|
||||||
try {
|
|
||||||
videoSocket = localServerSocket.accept();
|
videoSocket = localServerSocket.accept();
|
||||||
if (sendDummyByte) {
|
if (sendDummyByte) {
|
||||||
// send one byte so the client may read() to detect a connection error
|
// send one byte so the client may read() to detect a connection error
|
||||||
@@ -65,14 +75,12 @@ public final class DesktopConnection implements Closeable {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
localServerSocket.close();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
videoSocket = connect(SOCKET_NAME);
|
videoSocket = connect(socketName);
|
||||||
if (control) {
|
if (control) {
|
||||||
try {
|
try {
|
||||||
controlSocket = connect(SOCKET_NAME);
|
controlSocket = connect(socketName);
|
||||||
} catch (IOException | RuntimeException e) {
|
} catch (IOException | RuntimeException e) {
|
||||||
videoSocket.close();
|
videoSocket.close();
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public final class DeviceMessageSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void loop() throws IOException, InterruptedException {
|
public void loop() throws IOException, InterruptedException {
|
||||||
while (true) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
String text;
|
String text;
|
||||||
long sequence;
|
long sequence;
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
|
|||||||
40
server/src/main/java/com/genymobile/scrcpy/FakeContext.java
Normal file
40
server/src/main/java/com/genymobile/scrcpy/FakeContext.java
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.AttributionSource;
|
||||||
|
import android.content.ContextWrapper;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Process;
|
||||||
|
|
||||||
|
public final class FakeContext extends ContextWrapper {
|
||||||
|
|
||||||
|
public static final String PACKAGE_NAME = "com.android.shell";
|
||||||
|
|
||||||
|
private static final FakeContext INSTANCE = new FakeContext();
|
||||||
|
|
||||||
|
public static FakeContext get() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FakeContext() {
|
||||||
|
super(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPackageName() {
|
||||||
|
return PACKAGE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOpPackageName() {
|
||||||
|
return PACKAGE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.S)
|
||||||
|
@Override
|
||||||
|
public AttributionSource getAttributionSource() {
|
||||||
|
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
|
||||||
|
builder.setPackageName(PACKAGE_NAME);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import java.util.List;
|
|||||||
|
|
||||||
public class Options {
|
public class Options {
|
||||||
private Ln.Level logLevel = Ln.Level.DEBUG;
|
private Ln.Level logLevel = Ln.Level.DEBUG;
|
||||||
|
private int uid = -1; // 31-bit non-negative value, or -1
|
||||||
private int maxSize;
|
private int maxSize;
|
||||||
private int bitRate = 8000000;
|
private int bitRate = 8000000;
|
||||||
private int maxFps;
|
private int maxFps;
|
||||||
@@ -37,6 +38,14 @@ public class Options {
|
|||||||
this.logLevel = logLevel;
|
this.logLevel = logLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUid(int uid) {
|
||||||
|
this.uid = uid;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMaxSize() {
|
public int getMaxSize() {
|
||||||
return maxSize;
|
return maxSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package com.genymobile.scrcpy;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class Position {
|
public class Position {
|
||||||
private Point point;
|
private final Point point;
|
||||||
private Size screenSize;
|
private final Size screenSize;
|
||||||
|
|
||||||
public Position(Point point, Size screenSize) {
|
public Position(Point point, Size screenSize) {
|
||||||
this.point = point;
|
this.point = point;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import android.media.MediaCodecList;
|
|||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
@@ -27,6 +28,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
|
|
||||||
// Keep the values in descending order
|
// Keep the values in descending order
|
||||||
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
|
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
|
||||||
|
private static final int MAX_CONSECUTIVE_ERRORS = 3;
|
||||||
|
|
||||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||||
@@ -43,6 +45,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
private long ptsOrigin;
|
private long ptsOrigin;
|
||||||
|
|
||||||
private boolean firstFrameSent;
|
private boolean firstFrameSent;
|
||||||
|
private int consecutiveErrors;
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
||||||
boolean downsizeOnError) {
|
boolean downsizeOnError) {
|
||||||
@@ -64,39 +67,32 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||||
Workarounds.prepareMainLooper();
|
MediaCodec codec = createCodec(encoderName);
|
||||||
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
|
||||||
// <https://github.com/Genymobile/scrcpy/issues/240>
|
|
||||||
// <https://github.com/Genymobile/scrcpy/issues/2656>
|
|
||||||
Workarounds.fillAppInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
internalStreamScreen(device, fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
|
||||||
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
||||||
|
IBinder display = createDisplay();
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
MediaCodec codec = createCodec(encoderName);
|
|
||||||
IBinder display = createDisplay();
|
|
||||||
ScreenInfo screenInfo = device.getScreenInfo();
|
ScreenInfo screenInfo = device.getScreenInfo();
|
||||||
Rect contentRect = screenInfo.getContentRect();
|
Rect contentRect = screenInfo.getContentRect();
|
||||||
|
|
||||||
// include the locked video orientation
|
// include the locked video orientation
|
||||||
Rect videoRect = screenInfo.getVideoSize().toRect();
|
Rect videoRect = screenInfo.getVideoSize().toRect();
|
||||||
// does not include the locked video orientation
|
format.setInteger(MediaFormat.KEY_WIDTH, videoRect.width());
|
||||||
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height());
|
||||||
int videoRotation = screenInfo.getVideoRotation();
|
|
||||||
int layerStack = device.getLayerStack();
|
|
||||||
setSize(format, videoRect.width(), videoRect.height());
|
|
||||||
|
|
||||||
Surface surface = null;
|
Surface surface = null;
|
||||||
try {
|
try {
|
||||||
configure(codec, format);
|
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||||
surface = codec.createInputSurface();
|
surface = codec.createInputSurface();
|
||||||
|
|
||||||
|
// does not include the locked video orientation
|
||||||
|
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
||||||
|
int videoRotation = screenInfo.getVideoRotation();
|
||||||
|
int layerStack = device.getLayerStack();
|
||||||
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
||||||
|
|
||||||
codec.start();
|
codec.start();
|
||||||
|
|
||||||
alive = encode(codec, fd);
|
alive = encode(codec, fd);
|
||||||
@@ -104,34 +100,58 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
codec.stop();
|
codec.stop();
|
||||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||||
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
|
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
|
||||||
if (!downsizeOnError || firstFrameSent) {
|
if (!prepareRetry(device, screenInfo)) {
|
||||||
// Fail immediately
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
Ln.i("Retrying...");
|
||||||
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
|
|
||||||
if (newMaxSize == 0) {
|
|
||||||
// Definitively fail
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry with a smaller device size
|
|
||||||
Ln.i("Retrying with -m" + newMaxSize + "...");
|
|
||||||
device.setMaxSize(newMaxSize);
|
|
||||||
alive = true;
|
alive = true;
|
||||||
} finally {
|
} finally {
|
||||||
destroyDisplay(display);
|
codec.reset();
|
||||||
codec.release();
|
|
||||||
if (surface != null) {
|
if (surface != null) {
|
||||||
surface.release();
|
surface.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (alive);
|
} while (alive);
|
||||||
} finally {
|
} finally {
|
||||||
|
codec.release();
|
||||||
device.setRotationListener(null);
|
device.setRotationListener(null);
|
||||||
|
SurfaceControl.destroyDisplay(display);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean prepareRetry(Device device, ScreenInfo screenInfo) {
|
||||||
|
if (firstFrameSent) {
|
||||||
|
++consecutiveErrors;
|
||||||
|
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
||||||
|
// Definitively fail
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit to increase the probability that retrying will fix the problem
|
||||||
|
SystemClock.sleep(50);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!downsizeOnError) {
|
||||||
|
// Must fail immediately
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising)
|
||||||
|
|
||||||
|
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
|
||||||
|
Ln.i("newMaxSize = " + newMaxSize);
|
||||||
|
if (newMaxSize == 0) {
|
||||||
|
// Must definitively fail
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry with a smaller device size
|
||||||
|
Ln.i("Retrying with -m" + newMaxSize + "...");
|
||||||
|
device.setMaxSize(newMaxSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static int chooseMaxSizeFallback(Size failedSize) {
|
private static int chooseMaxSizeFallback(Size failedSize) {
|
||||||
int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight());
|
int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight());
|
||||||
for (int value : MAX_SIZE_FALLBACK) {
|
for (int value : MAX_SIZE_FALLBACK) {
|
||||||
@@ -167,6 +187,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
|
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
|
||||||
// If this is not a config packet, then it contains a frame
|
// If this is not a config packet, then it contains a frame
|
||||||
firstFrameSent = true;
|
firstFrameSent = true;
|
||||||
|
consecutiveErrors = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -278,15 +299,6 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return SurfaceControl.createDisplay("scrcpy", secure);
|
return SurfaceControl.createDisplay("scrcpy", secure);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void configure(MediaCodec codec, MediaFormat format) {
|
|
||||||
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setSize(MediaFormat format, int width, int height) {
|
|
||||||
format.setInteger(MediaFormat.KEY_WIDTH, width);
|
|
||||||
format.setInteger(MediaFormat.KEY_HEIGHT, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
|
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
|
||||||
SurfaceControl.openTransaction();
|
SurfaceControl.openTransaction();
|
||||||
try {
|
try {
|
||||||
@@ -297,8 +309,4 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
SurfaceControl.closeTransaction();
|
SurfaceControl.closeTransaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void destroyDisplay(IBinder display) {
|
|
||||||
SurfaceControl.destroyDisplay(display);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,11 +66,19 @@ public final class Server {
|
|||||||
|
|
||||||
Thread initThread = startInitThread(options);
|
Thread initThread = startInitThread(options);
|
||||||
|
|
||||||
|
Workarounds.prepareMainLooper();
|
||||||
|
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
||||||
|
// <https://github.com/Genymobile/scrcpy/issues/240>
|
||||||
|
// <https://github.com/Genymobile/scrcpy/issues/2656>
|
||||||
|
Workarounds.fillAppInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
int uid = options.getUid();
|
||||||
boolean tunnelForward = options.isTunnelForward();
|
boolean tunnelForward = options.isTunnelForward();
|
||||||
boolean control = options.getControl();
|
boolean control = options.getControl();
|
||||||
boolean sendDummyByte = options.getSendDummyByte();
|
boolean sendDummyByte = options.getSendDummyByte();
|
||||||
|
|
||||||
try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control, sendDummyByte)) {
|
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
|
||||||
if (options.getSendDeviceMeta()) {
|
if (options.getSendDeviceMeta()) {
|
||||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||||
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||||
@@ -87,12 +95,7 @@ public final class Server {
|
|||||||
controllerThread = startController(controller);
|
controllerThread = startController(controller);
|
||||||
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
|
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
|
||||||
|
|
||||||
device.setClipboardListener(new Device.ClipboardListener() {
|
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
|
||||||
@Override
|
|
||||||
public void onClipboardTextChanged(String text) {
|
|
||||||
controller.getSender().pushClipboardText(text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -114,26 +117,18 @@ public final class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Thread startInitThread(final Options options) {
|
private static Thread startInitThread(final Options options) {
|
||||||
Thread thread = new Thread(new Runnable() {
|
Thread thread = new Thread(() -> initAndCleanUp(options));
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
initAndCleanUp(options);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
thread.start();
|
thread.start();
|
||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Thread startController(final Controller controller) {
|
private static Thread startController(final Controller controller) {
|
||||||
Thread thread = new Thread(new Runnable() {
|
Thread thread = new Thread(() -> {
|
||||||
@Override
|
try {
|
||||||
public void run() {
|
controller.control();
|
||||||
try {
|
} catch (IOException e) {
|
||||||
controller.control();
|
// this is expected on close
|
||||||
} catch (IOException e) {
|
Ln.d("Controller stopped");
|
||||||
// this is expected on close
|
|
||||||
Ln.d("Controller stopped");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
thread.start();
|
thread.start();
|
||||||
@@ -141,15 +136,12 @@ public final class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
|
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
|
||||||
Thread thread = new Thread(new Runnable() {
|
Thread thread = new Thread(() -> {
|
||||||
@Override
|
try {
|
||||||
public void run() {
|
sender.loop();
|
||||||
try {
|
} catch (IOException | InterruptedException e) {
|
||||||
sender.loop();
|
// this is expected on close
|
||||||
} catch (IOException | InterruptedException e) {
|
Ln.d("Device message sender stopped");
|
||||||
// this is expected on close
|
|
||||||
Ln.d("Device message sender stopped");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
thread.start();
|
thread.start();
|
||||||
@@ -178,6 +170,13 @@ public final class Server {
|
|||||||
String key = arg.substring(0, equalIndex);
|
String key = arg.substring(0, equalIndex);
|
||||||
String value = arg.substring(equalIndex + 1);
|
String value = arg.substring(equalIndex + 1);
|
||||||
switch (key) {
|
switch (key) {
|
||||||
|
case "uid":
|
||||||
|
int uid = Integer.parseInt(value, 0x10);
|
||||||
|
if (uid < -1) {
|
||||||
|
throw new IllegalArgumentException("uid may not be negative (except -1 for 'none'): " + uid);
|
||||||
|
}
|
||||||
|
options.setUid(uid);
|
||||||
|
break;
|
||||||
case "log_level":
|
case "log_level":
|
||||||
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
||||||
options.setLogLevel(level);
|
options.setLogLevel(level);
|
||||||
@@ -319,12 +318,9 @@ public final class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String... args) throws Exception {
|
public static void main(String... args) throws Exception {
|
||||||
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
|
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||||
@Override
|
Ln.e("Exception on thread " + t, e);
|
||||||
public void uncaughtException(Thread t, Throwable e) {
|
suggestFix(e);
|
||||||
Ln.e("Exception on thread " + t, e);
|
|
||||||
suggestFix(e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Options options = createOptions(args);
|
Options options = createOptions(args);
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ package com.genymobile.scrcpy;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.app.Instrumentation;
|
import android.app.Instrumentation;
|
||||||
import android.content.Context;
|
import android.content.ContextWrapper;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
public final class Workarounds {
|
public final class Workarounds {
|
||||||
private Workarounds() {
|
private Workarounds() {
|
||||||
@@ -50,7 +49,7 @@ public final class Workarounds {
|
|||||||
Object appBindData = appBindDataConstructor.newInstance();
|
Object appBindData = appBindDataConstructor.newInstance();
|
||||||
|
|
||||||
ApplicationInfo applicationInfo = new ApplicationInfo();
|
ApplicationInfo applicationInfo = new ApplicationInfo();
|
||||||
applicationInfo.packageName = "com.genymobile.scrcpy";
|
applicationInfo.packageName = FakeContext.PACKAGE_NAME;
|
||||||
|
|
||||||
// appBindData.appInfo = applicationInfo;
|
// appBindData.appInfo = applicationInfo;
|
||||||
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
|
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
|
||||||
@@ -62,11 +61,10 @@ public final class Workarounds {
|
|||||||
mBoundApplicationField.setAccessible(true);
|
mBoundApplicationField.setAccessible(true);
|
||||||
mBoundApplicationField.set(activityThread, appBindData);
|
mBoundApplicationField.set(activityThread, appBindData);
|
||||||
|
|
||||||
// Context ctx = activityThread.getSystemContext();
|
Application app = Application.class.newInstance();
|
||||||
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
|
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
|
||||||
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
|
baseField.setAccessible(true);
|
||||||
|
baseField.set(app, FakeContext.get());
|
||||||
Application app = Instrumentation.newApplication(Application.class, ctx);
|
|
||||||
|
|
||||||
// activityThread.mInitialApplication = app;
|
// activityThread.mInitialApplication = app;
|
||||||
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.genymobile.scrcpy.Ln;
|
|||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
|
import android.os.Process;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
@@ -48,10 +49,10 @@ public class ActivityManager {
|
|||||||
Object[] args;
|
Object[] args;
|
||||||
if (getContentProviderExternalMethodNewVersion) {
|
if (getContentProviderExternalMethodNewVersion) {
|
||||||
// new version
|
// new version
|
||||||
args = new Object[]{name, ServiceManager.USER_ID, token, null};
|
args = new Object[]{name, Process.ROOT_UID, token, null};
|
||||||
} else {
|
} else {
|
||||||
// old version
|
// old version
|
||||||
args = new Object[]{name, ServiceManager.USER_ID, token};
|
args = new Object[]{name, Process.ROOT_UID, token};
|
||||||
}
|
}
|
||||||
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
|
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
|
||||||
Object providerHolder = method.invoke(manager, args);
|
Object providerHolder = method.invoke(manager, args);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.FakeContext;
|
||||||
import com.genymobile.scrcpy.Ln;
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.IOnPrimaryClipChangedListener;
|
import android.content.IOnPrimaryClipChangedListener;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
|
import android.os.Process;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -58,22 +60,22 @@ public class ClipboardManager {
|
|||||||
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
|
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
|
||||||
throws InvocationTargetException, IllegalAccessException {
|
throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
|
||||||
}
|
}
|
||||||
if (alternativeMethod) {
|
if (alternativeMethod) {
|
||||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID);
|
||||||
}
|
}
|
||||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, Process.ROOT_UID);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
|
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
|
||||||
throws InvocationTargetException, IllegalAccessException {
|
throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
|
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
|
||||||
} else if (alternativeMethod) {
|
} else if (alternativeMethod) {
|
||||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID);
|
||||||
} else {
|
} else {
|
||||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, Process.ROOT_UID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,11 +108,11 @@ public class ClipboardManager {
|
|||||||
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
|
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
|
||||||
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
|
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||||
} else if (alternativeMethod) {
|
} else if (alternativeMethod) {
|
||||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID);
|
||||||
} else {
|
} else {
|
||||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, Process.ROOT_UID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.FakeContext;
|
||||||
import com.genymobile.scrcpy.Ln;
|
import com.genymobile.scrcpy.Ln;
|
||||||
import com.genymobile.scrcpy.SettingsException;
|
import com.genymobile.scrcpy.SettingsException;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.AttributionSource;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.os.Process;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
@@ -51,11 +55,10 @@ public class ContentProvider implements Closeable {
|
|||||||
@SuppressLint("PrivateApi")
|
@SuppressLint("PrivateApi")
|
||||||
private Method getCallMethod() throws NoSuchMethodException {
|
private Method getCallMethod() throws NoSuchMethodException {
|
||||||
if (callMethod == null) {
|
if (callMethod == null) {
|
||||||
try {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
Class<?> attributionSourceClass = Class.forName("android.content.AttributionSource");
|
callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class);
|
||||||
callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class);
|
|
||||||
callMethodVersion = 0;
|
callMethodVersion = 0;
|
||||||
} catch (NoSuchMethodException | ClassNotFoundException e0) {
|
} else {
|
||||||
// old versions
|
// old versions
|
||||||
try {
|
try {
|
||||||
callMethod = provider.getClass()
|
callMethod = provider.getClass()
|
||||||
@@ -75,40 +78,29 @@ public class ContentProvider implements Closeable {
|
|||||||
return callMethod;
|
return callMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("PrivateApi")
|
|
||||||
private Object getAttributionSource()
|
|
||||||
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
|
||||||
if (attributionSource == null) {
|
|
||||||
Class<?> cl = Class.forName("android.content.AttributionSource$Builder");
|
|
||||||
Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID);
|
|
||||||
cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME);
|
|
||||||
attributionSource = cl.getDeclaredMethod("build").invoke(builder);
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributionSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bundle call(String callMethod, String arg, Bundle extras)
|
private Bundle call(String callMethod, String arg, Bundle extras)
|
||||||
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
|
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
|
||||||
try {
|
try {
|
||||||
Method method = getCallMethod();
|
Method method = getCallMethod();
|
||||||
Object[] args;
|
Object[] args;
|
||||||
switch (callMethodVersion) {
|
|
||||||
case 0:
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) {
|
||||||
args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras};
|
args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras};
|
||||||
break;
|
} else {
|
||||||
case 1:
|
switch (callMethodVersion) {
|
||||||
args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
|
case 1:
|
||||||
break;
|
args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
|
||||||
case 2:
|
break;
|
||||||
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
case 2:
|
||||||
break;
|
args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
||||||
default:
|
break;
|
||||||
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
|
default:
|
||||||
break;
|
args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras};
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (Bundle) method.invoke(provider, args);
|
return (Bundle) method.invoke(provider, args);
|
||||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke method", e);
|
Ln.e("Could not invoke method", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@@ -147,7 +139,7 @@ public class ContentProvider implements Closeable {
|
|||||||
public String getValue(String table, String key) throws SettingsException {
|
public String getValue(String table, String key) throws SettingsException {
|
||||||
String method = getGetMethod(table);
|
String method = getGetMethod(table);
|
||||||
Bundle arg = new Bundle();
|
Bundle arg = new Bundle();
|
||||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
arg.putInt(CALL_METHOD_USER_KEY, Process.ROOT_UID);
|
||||||
try {
|
try {
|
||||||
Bundle bundle = call(method, key, arg);
|
Bundle bundle = call(method, key, arg);
|
||||||
if (bundle == null) {
|
if (bundle == null) {
|
||||||
@@ -163,7 +155,7 @@ public class ContentProvider implements Closeable {
|
|||||||
public void putValue(String table, String key, String value) throws SettingsException {
|
public void putValue(String table, String key, String value) throws SettingsException {
|
||||||
String method = getPutMethod(table);
|
String method = getPutMethod(table);
|
||||||
Bundle arg = new Bundle();
|
Bundle arg = new Bundle();
|
||||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
arg.putInt(CALL_METHOD_USER_KEY, Process.ROOT_UID);
|
||||||
arg.putString(NAME_VALUE_TABLE_VALUE, value);
|
arg.putString(NAME_VALUE_TABLE_VALUE, value);
|
||||||
try {
|
try {
|
||||||
call(method, key, arg);
|
call(method, key, arg);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers;
|
|||||||
import com.genymobile.scrcpy.Ln;
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
import android.view.InputEvent;
|
import android.view.InputEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -17,6 +18,7 @@ public final class InputManager {
|
|||||||
private Method injectInputEventMethod;
|
private Method injectInputEventMethod;
|
||||||
|
|
||||||
private static Method setDisplayIdMethod;
|
private static Method setDisplayIdMethod;
|
||||||
|
private static Method setActionButtonMethod;
|
||||||
|
|
||||||
public InputManager(android.hardware.input.InputManager manager) {
|
public InputManager(android.hardware.input.InputManager manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@@ -56,4 +58,22 @@ public final class InputManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Method getSetActionButtonMethod() throws NoSuchMethodException {
|
||||||
|
if (setActionButtonMethod == null) {
|
||||||
|
setActionButtonMethod = MotionEvent.class.getMethod("setActionButton", int.class);
|
||||||
|
}
|
||||||
|
return setActionButtonMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean setActionButton(MotionEvent motionEvent, int actionButton) {
|
||||||
|
try {
|
||||||
|
Method method = getSetActionButtonMethod();
|
||||||
|
method.invoke(motionEvent, actionButton);
|
||||||
|
return true;
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
Ln.e("Cannot set action button on MotionEvent", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ import java.lang.reflect.Method;
|
|||||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||||
public final class ServiceManager {
|
public final class ServiceManager {
|
||||||
|
|
||||||
public static final String PACKAGE_NAME = "com.android.shell";
|
|
||||||
public static final int USER_ID = 0;
|
|
||||||
|
|
||||||
private static final Method GET_SERVICE_METHOD;
|
private static final Method GET_SERVICE_METHOD;
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ public class ControlMessageReaderTest {
|
|||||||
dos.writeShort(1080);
|
dos.writeShort(1080);
|
||||||
dos.writeShort(1920);
|
dos.writeShort(1920);
|
||||||
dos.writeShort(0xffff); // pressure
|
dos.writeShort(0xffff); // pressure
|
||||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // action button
|
||||||
|
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons
|
||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
@@ -112,6 +113,7 @@ public class ControlMessageReaderTest {
|
|||||||
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
|
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
|
||||||
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
|
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
|
||||||
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
|
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
|
||||||
|
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton());
|
||||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
|
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user