Compare commits

..

2 Commits

Author SHA1 Message Date
Romain Vimont
2b1fd581fd Use meson feature DONOTMERGEYET
Use: https://mesonbuild.com/Reference-manual_returned_feature.html#featurerequire
once we require meson 0.59
2023-01-06 11:29:42 +01:00
Romain Vimont
ebf8c4dbf9 Add FAQ section about HID/OTG on Windows
Refs #3654 <https://github.com/Genymobile/scrcpy/issues/3654>
2023-01-06 10:44:13 +01:00
39 changed files with 251 additions and 758 deletions

17
FAQ.md
View File

@@ -7,7 +7,7 @@ Here are the common reported problems and their status.
If you encounter any error, the first step is to upgrade to the latest version. If you encounter any error, the first step is to upgrade to the latest version.
## `adb` issues ## `adb` and USB issues
`scrcpy` execute `adb` commands to initialize the connection with the device. If `scrcpy` execute `adb` commands to initialize the connection with the device. If
`adb` fails, then scrcpy will not work. `adb` fails, then scrcpy will not work.
@@ -133,6 +133,21 @@ Try with another USB cable or plug it into another USB port. See [#281] and
[#283]: https://github.com/Genymobile/scrcpy/issues/283 [#283]: https://github.com/Genymobile/scrcpy/issues/283
## HID/OTG issues on Windows
On Windows, if `scrcpy --otg` (or `--hid-keyboard`/`--hid-mouse`) results in:
> ERROR: Could not find any USB device
(or if only unrelated USB devices are detected), there might be drivers issues.
Please read [#3654], in particular [this comment][#3654-comment1] and [the next
one][#3654-comment2].
[#3654]: https://github.com/Genymobile/scrcpy/issues/3654
[#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232
[#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011
## Control issues ## Control issues

View File

@@ -37,7 +37,6 @@ 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',
@@ -70,12 +69,12 @@ else
endif endif
endif endif
v4l2_support = get_option('v4l2') and host_machine.system() == 'linux' v4l2_support = get_option('v4l2').enabled() and host_machine.system() == 'linux'
if v4l2_support if v4l2_support
src += [ 'src/v4l2_sink.c' ] src += [ 'src/v4l2_sink.c' ]
endif endif
usb_support = get_option('usb') usb_support = get_option('usb').enabled()
if usb_support if usb_support
src += [ src += [
'src/usb/aoa_hid.c', 'src/usb/aoa_hid.c',
@@ -171,8 +170,6 @@ check_functions = [
'strdup', 'strdup',
'asprintf', 'asprintf',
'vasprintf', 'vasprintf',
'nrand48',
'jrand48',
] ]
foreach f : check_functions foreach f : check_functions

View File

@@ -7,6 +7,8 @@
#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);
@@ -15,11 +17,10 @@ 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, device_socket_name, port, if (!sc_adb_reverse(intr, serial, SC_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;
@@ -51,7 +52,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, device_socket_name, if (!sc_adb_reverse_remove(intr, serial, SC_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);
} }
@@ -77,13 +78,12 @@ 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, device_socket_name, if (sc_adb_forward(intr, serial, port, SC_SOCKET_NAME,
SC_ADB_NO_STDOUT)) { SC_ADB_NO_STDOUT)) {
// success // success
tunnel->local_port = port; tunnel->local_port = port;
@@ -123,14 +123,13 @@ 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, const char *device_socket_name, const char *serial, struct sc_port_range port_range,
struct sc_port_range port_range, bool force_adb_forward) { 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, if (enable_tunnel_reverse_any_port(tunnel, intr, serial, port_range)) {
device_socket_name, port_range)) {
return true; return true;
} }
@@ -140,13 +139,12 @@ 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, return enable_tunnel_forward_any_port(tunnel, intr, serial, port_range);
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 *device_socket_name) { const char *serial) {
assert(tunnel->enabled); assert(tunnel->enabled);
bool ret; bool ret;
@@ -154,7 +152,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, device_socket_name, ret = sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME,
SC_ADB_NO_STDOUT); SC_ADB_NO_STDOUT);
assert(tunnel->server_socket != SC_SOCKET_NONE); assert(tunnel->server_socket != SC_SOCKET_NONE);

View File

@@ -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, const char *device_socket_name, const char *serial, struct sc_port_range port_range,
struct sc_port_range port_range, bool force_adb_forward); 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 *device_socket_name); const char *serial);
#endif #endif

View File

@@ -51,47 +51,3 @@ 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

View File

@@ -59,12 +59,4 @@ 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

View File

@@ -117,9 +117,8 @@ 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.action_button); sc_write32be(&buf[24], msg->inject_touch_event.buttons);
sc_write32be(&buf[28], msg->inject_touch_event.buttons); return 28;
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 =
@@ -180,25 +179,22 @@ 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 action_button=%06lx buttons=%06lx", " pressure=%f 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 action_button=%06lx" PRIi32 " pressure=%f buttons=%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;

View File

@@ -65,7 +65,6 @@ 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;

View File

@@ -339,7 +339,6 @@ 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)) {

View File

@@ -93,7 +93,6 @@ 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),
}, },
}; };

View File

@@ -11,8 +11,6 @@
/** Downcast packet_sink to recorder */ /** Downcast packet_sink to recorder */
#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink) #define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink)
#define SC_PTS_ORIGIN_NONE UINT64_C(-1)
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
static const AVOutputFormat * static const AVOutputFormat *
@@ -130,8 +128,6 @@ sc_recorder_write(struct sc_recorder *recorder, AVPacket *packet) {
return true; return true;
} }
LOGI("==== %" PRIu64, packet->pts);
sc_recorder_rescale_packet(recorder, packet); sc_recorder_rescale_packet(recorder, packet);
return av_write_frame(recorder->ctx, packet) >= 0; return av_write_frame(recorder->ctx, packet) >= 0;
} }
@@ -173,18 +169,6 @@ run_recorder(void *data) {
sc_mutex_unlock(&recorder->mutex); sc_mutex_unlock(&recorder->mutex);
if (recorder->pts_origin == SC_PTS_ORIGIN_NONE
&& rec->packet->pts != AV_NOPTS_VALUE) {
// First PTS received
recorder->pts_origin = rec->packet->pts;
}
if (rec->packet->pts != AV_NOPTS_VALUE) {
// Set PTS relatve to the origin
rec->packet->pts -= recorder->pts_origin;
rec->packet->dts = rec->packet->pts;
}
// recorder->previous is only written from this thread, no need to lock // recorder->previous is only written from this thread, no need to lock
struct sc_record_packet *previous = recorder->previous; struct sc_record_packet *previous = recorder->previous;
recorder->previous = rec; recorder->previous = rec;
@@ -259,7 +243,6 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
recorder->failed = false; recorder->failed = false;
recorder->header_written = false; recorder->header_written = false;
recorder->previous = NULL; recorder->previous = NULL;
recorder->pts_origin = SC_PTS_ORIGIN_NONE;
const char *format_name = sc_recorder_get_format_name(recorder->format); const char *format_name = sc_recorder_get_format_name(recorder->format);
assert(format_name); assert(format_name);

View File

@@ -28,8 +28,6 @@ struct sc_recorder {
struct sc_size declared_frame_size; struct sc_size declared_frame_size;
bool header_written; bool header_written;
uint64_t pts_origin;
sc_thread thread; sc_thread thread;
sc_mutex mutex; sc_mutex mutex;
sc_cond queue_cond; sc_cond queue_cond;

View File

@@ -32,7 +32,6 @@
#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
@@ -266,14 +265,6 @@ 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;
@@ -307,10 +298,7 @@ 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,

View File

@@ -20,7 +20,6 @@
#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) {
@@ -198,7 +197,6 @@ 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);
@@ -366,7 +364,6 @@ 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;
@@ -466,8 +463,7 @@ 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);
@@ -498,8 +494,7 @@ 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;
@@ -769,23 +764,13 @@ 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,
server->device_socket_name, params->port_range, params->port_range, params->force_adb_forward);
params->force_adb_forward);
if (!ok) { if (!ok) {
goto error_connection_failed; goto error_connection_failed;
} }
@@ -793,8 +778,7 @@ 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;
} }
@@ -806,8 +790,7 @@ 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;
} }
@@ -901,7 +884,6 @@ 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);

View File

@@ -22,7 +22,6 @@ 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;
@@ -55,7 +54,6 @@ 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

View File

@@ -1,24 +0,0 @@
#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;
}

View File

@@ -1,16 +0,0 @@
#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

View File

@@ -90,14 +90,13 @@ 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 == 32); assert(size == 28);
const unsigned char expected[] = { const unsigned char expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
@@ -106,8 +105,7 @@ 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 (action button) 0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons)
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }

View File

@@ -4,5 +4,5 @@ option('prebuilt_server', type: 'string', description: 'Path of the prebuilt ser
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")') option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")')
option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') option('v4l2', type: 'feature', value: 'enabled', description: 'Enable V4L2 feature when supported')
option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') option('usb', type: 'feature', value: 'enabled', description: 'Enable HID/OTG features when supported')

View File

@@ -20,21 +20,18 @@ 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" "$GEN_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
mkdir -p "$CLASSES_DIR" mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
mkdir -p "$GEN_DIR/com/genymobile/scrcpy"
<< EOF cat > "$GEN_DIR/com/genymobile/scrcpy/BuildConfig.java" << EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java"
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
public final class BuildConfig { public final class BuildConfig {
@@ -45,15 +42,13 @@ 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"$GEN_DIR" android/view/IRotationWatcher.aidl "$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" android/view/IRotationWatcher.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \ "$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_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" \ javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \
-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
@@ -73,7 +68,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 rm -rf classes.dex classes
else else
# use d8 # use d8
"$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \ "$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \
@@ -85,8 +80,7 @@ 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"

View File

@@ -1,196 +0,0 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
public final class AudioEncoder {
private static final String MIMETYPE = MediaFormat.MIMETYPE_AUDIO_OPUS;
private static final int SAMPLE_RATE = 48000;
private static final int CHANNELS = 2;
private static final int BIT_RATE = 128000;
private static int BUFFER_MS = 15; // milliseconds
private static final int BUFFER_SIZE = SAMPLE_RATE * CHANNELS * BUFFER_MS / 1000;
private AudioRecord recorder;
private MediaCodec mediaCodec;
private HandlerThread thread;
private final AtomicBoolean interrupted = new AtomicBoolean();
private final Semaphore endSemaphore = new Semaphore(0); // blocks until encoding is ended
private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(AudioFormat.ENCODING_PCM_16BIT);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNELS == 2 ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO);
return builder.build();
}
@TargetApi(Build.VERSION_CODES.M)
@SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord() {
AudioRecord.Builder builder = new AudioRecord.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
builder.setAudioFormat(createAudioFormat());
builder.setBufferSizeInBytes(1024 * 1024);
return builder.build();
}
private static MediaFormat createFormat() {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MIMETYPE);
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
return format;
}
@TargetApi(Build.VERSION_CODES.M)
public void start() throws IOException {
mediaCodec = MediaCodec.createEncoderByType(MIMETYPE); // may throw IOException
recorder = createAudioRecord();
MediaFormat format = createFormat();
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
recorder.startRecording();
thread = new HandlerThread("AudioEncoder");
thread.start();
class AudioEncoderCallbacks extends MediaCodec.Callback {
private final AudioTimestamp timestamp = new AudioTimestamp();
private long nextPts;
private boolean eofSignaled;
private boolean ended;
private void notifyEnded() {
assert(!ended);
ended = true;
endSemaphore.release();
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
if (eofSignaled) {
return;
}
ByteBuffer inputBuffer = codec.getInputBuffer(index);
int r = recorder.read(inputBuffer, BUFFER_SIZE);
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS) {
pts = timestamp.nanoTime / 1000;
} else {
if (nextPts == 0) {
Ln.w("Could not get any audio timestamp");
}
// compute from previous timestamp and packet size
pts = nextPts;
}
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
nextPts = pts + durationMs;
int flags = 0;
if (interrupted.get()) {
flags = flags | MediaCodec.BUFFER_FLAG_END_OF_STREAM;
eofSignaled = true;
}
codec.queueInputBuffer(index, 0, r, pts, flags);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) {
if (ended) {
return;
}
ByteBuffer codecBuffer = codec.getOutputBuffer(index);
try {
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
long pts = bufferInfo.presentationTimeUs;
Ln.i("Audio packet: pts=" + pts + " " + codecBuffer.remaining() + " bytes");
} finally {
codec.releaseOutputBuffer(index, false);
}
boolean eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (eof) {
notifyEnded();
}
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
Ln.e("MediaCodec error", e);
if (!ended) {
notifyEnded();
}
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// ignore
}
}
mediaCodec.setCallback(new AudioEncoderCallbacks(), new Handler(thread.getLooper()));
mediaCodec.start();
}
private void waitEnded() {
try {
endSemaphore.acquire();
} catch (InterruptedException e) {
// ignore
}
}
public void stop() {
Ln.i("==== STOP");
if (thread != null) {
interrupted.set(true);
waitEnded();
thread.interrupt();
thread = null;
mediaCodec.stop();
mediaCodec.release();
recorder.stop();
Ln.i("==== STOPPED");
}
}
}

View File

@@ -4,8 +4,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public class CodecOption { public class CodecOption {
private final String key; private String key;
private final Object value; private Object value;
public CodecOption(String key, Object value) { public CodecOption(String key, Object value) {
this.key = key; this.key = key;

View File

@@ -29,7 +29,6 @@ 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;
@@ -61,15 +60,13 @@ public final class ControlMessage {
return msg; return msg;
} }
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton, public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
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;
} }
@@ -143,10 +140,6 @@ public final class ControlMessage {
return keycode; return keycode;
} }
public int getActionButton() {
return actionButton;
}
public int getButtons() { public int getButtons() {
return buttons; return buttons;
} }

View File

@@ -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 = 31; static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
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,9 +140,8 @@ 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, actionButton, buttons); return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
} }
private ControlMessage parseInjectScrollEvent() { private ControlMessage parseInjectScrollEvent() {

View File

@@ -1,7 +1,5 @@
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;
@@ -24,8 +22,6 @@ public class Controller {
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private Thread thread;
private final Device device; private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
@@ -64,7 +60,7 @@ public class Controller {
} }
} }
private void control() throws IOException { public void control() throws IOException {
// on start, power on the device // on start, power on the device
if (powerOn && !Device.isScreenOn()) { if (powerOn && !Device.isScreenOn()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
@@ -79,32 +75,11 @@ public class Controller {
SystemClock.sleep(500); SystemClock.sleep(500);
} }
while (!Thread.currentThread().isInterrupted()) { while (true) {
handleEvent(); handleEvent();
} }
} }
public void start() {
thread = new Thread(() -> {
try {
control();
} catch (IOException e) {
// this is expected on close
Ln.d("Controller stopped");
}
});
thread.start();
sender.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
thread = null;
}
sender.stop();
}
public DeviceMessageSender getSender() { public DeviceMessageSender getSender() {
return sender; return sender;
} }
@@ -124,7 +99,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.getActionButton(), msg.getButtons()); injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
} }
break; break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT: case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
@@ -204,7 +179,7 @@ public class Controller {
return successCount; return successCount;
} }
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
long now = SystemClock.uptimeMillis(); long now = SystemClock.uptimeMillis();
Point point = device.getPhysicalPoint(position); Point point = device.getPhysicalPoint(position);
@@ -221,23 +196,22 @@ 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;
@@ -251,62 +225,6 @@ 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);
@@ -340,9 +258,12 @@ 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(() -> { EXECUTOR.schedule(new Runnable() {
@Override
public void run() {
Ln.i("Forcing screen off"); Ln.i("Forcing screen off");
Device.setScreenPowerMode(Device.POWER_MODE_OFF); Device.setScreenPowerMode(Device.POWER_MODE_OFF);
}
}, 200, TimeUnit.MILLISECONDS); }, 200, TimeUnit.MILLISECONDS);
} }

View File

@@ -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_PREFIX = "scrcpy"; private static final String SOCKET_NAME = "scrcpy";
private final LocalSocket videoSocket; private final LocalSocket videoSocket;
private final FileDescriptor videoFd; private final FileDescriptor videoFd;
@@ -46,22 +46,12 @@ public final class DesktopConnection implements Closeable {
return localSocket; return localSocket;
} }
private static String getSocketName(int uid) { public static DesktopConnection open(boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
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) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
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
@@ -75,12 +65,14 @@ public final class DesktopConnection implements Closeable {
throw e; throw e;
} }
} }
} finally {
localServerSocket.close();
} }
} else { } else {
videoSocket = connect(socketName); videoSocket = connect(SOCKET_NAME);
if (control) { if (control) {
try { try {
controlSocket = connect(socketName); controlSocket = connect(SOCKET_NAME);
} catch (IOException | RuntimeException e) { } catch (IOException | RuntimeException e) {
videoSocket.close(); videoSocket.close();
throw e; throw e;

View File

@@ -6,8 +6,6 @@ public final class DeviceMessageSender {
private final DesktopConnection connection; private final DesktopConnection connection;
private Thread thread;
private String clipboardText; private String clipboardText;
private long ack; private long ack;
@@ -26,8 +24,8 @@ public final class DeviceMessageSender {
notify(); notify();
} }
private void loop() throws IOException, InterruptedException { public void loop() throws IOException, InterruptedException {
while (!Thread.currentThread().isInterrupted()) { while (true) {
String text; String text;
long sequence; long sequence;
synchronized (this) { synchronized (this) {
@@ -51,22 +49,4 @@ public final class DeviceMessageSender {
} }
} }
} }
public void start() {
thread = new Thread(() -> {
try {
loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
Ln.d("Device message sender stopped");
}
});
thread.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
thread = null;
}
}
} }

View File

@@ -1,40 +0,0 @@
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();
}
}

View File

@@ -6,7 +6,6 @@ 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;
@@ -38,14 +37,6 @@ 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;
} }

View File

@@ -3,8 +3,8 @@ package com.genymobile.scrcpy;
import java.util.Objects; import java.util.Objects;
public class Position { public class Position {
private final Point point; private Point point;
private final Size screenSize; private Size screenSize;
public Position(Point point, Size screenSize) { public Position(Point point, Size screenSize) {
this.point = point; this.point = point;

View File

@@ -9,7 +9,6 @@ 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;
@@ -28,7 +27,6 @@ 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;
@@ -42,9 +40,9 @@ public class ScreenEncoder implements Device.RotationListener {
private final int maxFps; private final int maxFps;
private final boolean sendFrameMeta; private final boolean sendFrameMeta;
private final boolean downsizeOnError; private final boolean downsizeOnError;
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) {
@@ -66,32 +64,39 @@ public class ScreenEncoder implements Device.RotationListener {
} }
public void streamScreen(Device device, FileDescriptor fd) throws IOException { public void streamScreen(Device device, FileDescriptor fd) throws IOException {
MediaCodec codec = createCodec(encoderName); Workarounds.prepareMainLooper();
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();
format.setInteger(MediaFormat.KEY_WIDTH, videoRect.width());
format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height());
Surface surface = null;
try {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = codec.createInputSurface();
// does not include the locked video orientation // does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation(); int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack(); int layerStack = device.getLayerStack();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); setSize(format, videoRect.width(), videoRect.height());
Surface surface = null;
try {
configure(codec, format);
surface = codec.createInputSurface();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
codec.start(); codec.start();
alive = encode(codec, fd); alive = encode(codec, fd);
@@ -99,58 +104,34 @@ 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 (!prepareRetry(device, screenInfo)) { if (!downsizeOnError || firstFrameSent) {
// 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 {
codec.reset(); destroyDisplay(display);
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) {
@@ -186,7 +167,6 @@ 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 {
@@ -206,7 +186,10 @@ public class ScreenEncoder implements Device.RotationListener {
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
pts = PACKET_FLAG_CONFIG; // non-media data packet pts = PACKET_FLAG_CONFIG; // non-media data packet
} else { } else {
pts = bufferInfo.presentationTimeUs; if (ptsOrigin == 0) {
ptsOrigin = bufferInfo.presentationTimeUs;
}
pts = bufferInfo.presentationTimeUs - ptsOrigin;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
pts |= PACKET_FLAG_KEY_FRAME; pts |= PACKET_FLAG_KEY_FRAME;
} }
@@ -295,6 +278,15 @@ 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 {
@@ -305,4 +297,8 @@ public class ScreenEncoder implements Device.RotationListener {
SurfaceControl.closeTransaction(); SurfaceControl.closeTransaction();
} }
} }
private static void destroyDisplay(IBinder display) {
SurfaceControl.destroyDisplay(display);
}
} }

View File

@@ -4,7 +4,6 @@ import android.graphics.Rect;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.os.BatteryManager; import android.os.BatteryManager;
import android.os.Build; import android.os.Build;
import android.provider.MediaStore;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@@ -67,28 +66,11 @@ public final class Server {
Thread initThread = startInitThread(options); Thread initThread = startInitThread(options);
int uid = options.getUid();
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
boolean control = options.getControl(); boolean control = options.getControl();
boolean audio = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; // TODO option
boolean sendDummyByte = options.getSendDummyByte(); boolean sendDummyByte = options.getSendDummyByte();
Workarounds.prepareMainLooper(); try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control, sendDummyByte)) {
// <https://github.com/Genymobile/scrcpy/issues/240>
// <https://github.com/Genymobile/scrcpy/issues/2656>
boolean mustFillAppInfo = Build.BRAND.equalsIgnoreCase("meizu");
// Before Android 11, audio is not supported.
// Since Android 12, we can properly set a context on the AudioRecord.
// Only on Android 11 we must fill app info for the AudioRecord to work.
mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R;
if (mustFillAppInfo) {
Workarounds.fillAppInfo();
}
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());
@@ -96,19 +78,21 @@ public final class Server {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
options.getEncoderName(), options.getDownsizeOnError()); options.getEncoderName(), options.getDownsizeOnError());
Controller controller = null; Thread controllerThread = null;
Thread deviceMessageSenderThread = null;
if (control) { if (control) {
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); final Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
controller.start();
final Controller controllerRef = controller; // asynchronous
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text)); controllerThread = startController(controller);
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
device.setClipboardListener(new Device.ClipboardListener() {
@Override
public void onClipboardTextChanged(String text) {
controller.getSender().pushClipboardText(text);
} }
});
AudioEncoder audioEncoder = null;
if (audio) {
audioEncoder = new AudioEncoder();
audioEncoder.start();
} }
try { try {
@@ -119,18 +103,55 @@ public final class Server {
Ln.d("Screen streaming stopped"); Ln.d("Screen streaming stopped");
} finally { } finally {
initThread.interrupt(); initThread.interrupt();
if (controller != null) { if (controllerThread != null) {
controller.stop(); controllerThread.interrupt();
} }
if (audioEncoder != null) { if (deviceMessageSenderThread != null) {
audioEncoder.stop(); deviceMessageSenderThread.interrupt();
} }
} }
} }
} }
private static Thread startInitThread(final Options options) { private static Thread startInitThread(final Options options) {
Thread thread = new Thread(() -> initAndCleanUp(options)); Thread thread = new Thread(new Runnable() {
@Override
public void run() {
initAndCleanUp(options);
}
});
thread.start();
return thread;
}
private static Thread startController(final Controller controller) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
controller.control();
} catch (IOException e) {
// this is expected on close
Ln.d("Controller stopped");
}
}
});
thread.start();
return thread;
}
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
sender.loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
Ln.d("Device message sender stopped");
}
}
});
thread.start(); thread.start();
return thread; return thread;
} }
@@ -157,13 +178,6 @@ 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);
@@ -305,9 +319,12 @@ public final class Server {
} }
public static void main(String... args) throws Exception { public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
Ln.e("Exception on thread " + t, e); Ln.e("Exception on thread " + t, e);
suggestFix(e); suggestFix(e);
}
}); });
Options options = createOptions(args); Options options = createOptions(args);

View File

@@ -3,12 +3,13 @@ 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.ContextWrapper; import android.content.Context;
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() {
@@ -49,7 +50,7 @@ public final class Workarounds {
Object appBindData = appBindDataConstructor.newInstance(); Object appBindData = appBindDataConstructor.newInstance();
ApplicationInfo applicationInfo = new ApplicationInfo(); ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.packageName = FakeContext.PACKAGE_NAME; applicationInfo.packageName = "com.genymobile.scrcpy";
// appBindData.appInfo = applicationInfo; // appBindData.appInfo = applicationInfo;
Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
@@ -61,10 +62,11 @@ public final class Workarounds {
mBoundApplicationField.setAccessible(true); mBoundApplicationField.setAccessible(true);
mBoundApplicationField.set(activityThread, appBindData); mBoundApplicationField.set(activityThread, appBindData);
Application app = Application.class.newInstance(); // Context ctx = activityThread.getSystemContext();
Field baseField = ContextWrapper.class.getDeclaredField("mBase"); Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
baseField.setAccessible(true); Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
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");

View File

@@ -5,7 +5,6 @@ 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;
@@ -49,10 +48,10 @@ public class ActivityManager {
Object[] args; Object[] args;
if (getContentProviderExternalMethodNewVersion) { if (getContentProviderExternalMethodNewVersion) {
// new version // new version
args = new Object[]{name, Process.ROOT_UID, token, null}; args = new Object[]{name, ServiceManager.USER_ID, token, null};
} else { } else {
// old version // old version
args = new Object[]{name, Process.ROOT_UID, token}; args = new Object[]{name, ServiceManager.USER_ID, token};
} }
// ContentProviderHolder providerHolder = getContentProviderExternal(...); // ContentProviderHolder providerHolder = getContentProviderExternal(...);
Object providerHolder = method.invoke(manager, args); Object providerHolder = method.invoke(manager, args);

View File

@@ -1,13 +1,11 @@
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;
@@ -60,22 +58,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, FakeContext.PACKAGE_NAME); return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
} }
if (alternativeMethod) { if (alternativeMethod) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID); return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
} }
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, Process.ROOT_UID); return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
} }
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, FakeContext.PACKAGE_NAME); method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
} else if (alternativeMethod) { } else if (alternativeMethod) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID); method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
} else { } else {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, Process.ROOT_UID); method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
} }
} }
@@ -108,11 +106,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, FakeContext.PACKAGE_NAME); method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
} else if (alternativeMethod) { } else if (alternativeMethod) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID); method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
} else { } else {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, Process.ROOT_UID); method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
} }
} }

View File

@@ -1,15 +1,11 @@
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;
@@ -55,10 +51,11 @@ 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) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { try {
callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class); Class<?> attributionSourceClass = Class.forName("android.content.AttributionSource");
callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class);
callMethodVersion = 0; callMethodVersion = 0;
} else { } catch (NoSuchMethodException | ClassNotFoundException e0) {
// old versions // old versions
try { try {
callMethod = provider.getClass() callMethod = provider.getClass()
@@ -78,29 +75,40 @@ 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 NoSuchMethodException, InvocationTargetException, IllegalAccessException { throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
try { try {
Method method = getCallMethod(); Method method = getCallMethod();
Object[] args; Object[] args;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) {
args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras};
} else {
switch (callMethodVersion) { switch (callMethodVersion) {
case 0:
args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras};
break;
case 1: case 1:
args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
break; break;
case 2: case 2:
args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras}; args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
break; break;
default: default:
args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras}; args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
break; break;
} }
}
return (Bundle) method.invoke(provider, args); return (Bundle) method.invoke(provider, args);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
throw e; throw e;
} }
@@ -139,7 +147,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, Process.ROOT_UID); arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
try { try {
Bundle bundle = call(method, key, arg); Bundle bundle = call(method, key, arg);
if (bundle == null) { if (bundle == null) {
@@ -155,7 +163,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, Process.ROOT_UID); arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
arg.putString(NAME_VALUE_TABLE_VALUE, value); arg.putString(NAME_VALUE_TABLE_VALUE, value);
try { try {
call(method, key, arg); call(method, key, arg);

View File

@@ -3,7 +3,6 @@ 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;
@@ -18,7 +17,6 @@ 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;
@@ -58,22 +56,4 @@ 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;
}
}
} }

View File

@@ -10,6 +10,9 @@ 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 {

View File

@@ -94,8 +94,7 @@ 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); // action button dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@@ -113,7 +112,6 @@ 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());
} }