Compare commits

...

51 Commits

Author SHA1 Message Date
Romain Vimont 6abff46c9f Add option to disable clipboard autosync
By default, scrcpy automatically synchronizes the computer clipboard to
the device clipboard before injecting Ctrl+v, and the device clipboard
to the computer clipboard whenever it changes.

This new option --no-clipboard-autosync disables this automatic
synchronization.

Fixes #2228 <https://github.com/Genymobile/scrcpy/issues/2228>
PR #2817 <https://github.com/Genymobile/scrcpy/pull/2817>
2021-11-24 09:44:39 +01:00
Romain Vimont 5d17bcf1bc Wait SET_CLIPBOARD ack before Ctrl+v via HID
To allow seamless copy-paste, on Ctrl+v, a SET_CLIPBOARD request is
performed before injecting Ctrl+v.

But when HID keyboard is enabled, the Ctrl+v injection is not sent on
the same channel as the clipboard request, so they are not serialized,
and may occur in any order. If Ctrl+v happens to be injected before the
new clipboard content is set, then the old content is pasted instead,
which is incorrect.

To minimize the probability of occurrence of the wrong order, a delay of
2 milliseconds was added before injecting Ctrl+v. Then 5ms. But even
with 5ms, the wrong behavior sometimes happens.

To handle it properly, add an acknowledgement mechanism, so that Ctrl+v
is injected over AOA only after the SET_CLIPBOARD request has been
performed and acknowledged by the server.

Refs e4163321f0
Refs 45b0f8123a

PR #2814 <https://github.com/Genymobile/scrcpy/pull/2814>
2021-11-24 09:41:21 +01:00
Romain Vimont 2d5525eac1 Move PRIu64 Windows workaround to compat.h
So that we can use it from several files.

PR #2814 <https://github.com/Genymobile/scrcpy/pull/2814>
2021-11-23 21:15:05 +01:00
Romain Vimont 41abe021e2 Make the device acknowledge device clipboard
If the client provided a sequence number on SET_CLIPBOARD request, make
the device send back an acknowledgement once the clipboard is set.

PR #2814 <https://github.com/Genymobile/scrcpy/pull/2814>
2021-11-23 21:15:05 +01:00
Romain Vimont 2a0730ee9b Add device clipboard set acknowledgement
Add a device message type so that the device could send acknowledgements
for SET_CLIPBOARD requests.

PR #2814 <https://github.com/Genymobile/scrcpy/pull/2814>
2021-11-23 21:15:05 +01:00
Romain Vimont 901d837165 Add sequence number to set_clipboard request
This will allow the client to request an acknowledgement.

PR #2814 <https://github.com/Genymobile/scrcpy/pull/2814>
2021-11-23 21:15:05 +01:00
Romain Vimont aba1fc03c3 Add acksync helper to wait for acks
This will allow to send requests with sequence numbers to the server
and wait for acknowledgements.

PR #2814 <https://github.com/Genymobile/scrcpy/pull/2814>
2021-11-23 21:15:05 +01:00
Romain Vimont 5b3856c3b6 Explicitly indicate when device clipboard is set
Pass the information that device clipboard has been set to the key
processor. This avoids the keyprocessor to "guess", and paves the way to
implement a proper acknowledgement mechanism.

PR #2814 <https://github.com/Genymobile/scrcpy/pull/2814>
2021-11-23 21:15:05 +01:00
Romain Vimont 854de9659a Do not inject Ctrl+v if clipboard sync failed
This prevents to paste the current Android clipboard, which would be
unexpected.

PR #2814 <https://github.com/Genymobile/scrcpy/pull/2814>
2021-11-23 21:15:05 +01:00
Romain Vimont ea8028332c Synchronize computer-to-device empty clipboard
Set the device clipboard to empty string if necessary. Otherwise, the
current device clipboard will be pasted on Ctrl+v.

PR #2814 <https://github.com/Genymobile/scrcpy/pull/2814>
2021-11-23 21:14:49 +01:00
Romain Vimont 0427a981e5 Use UINT64_C macro for uint64_t constant in tests
A long constant might not be sufficient.
2021-11-21 22:57:33 +01:00
Romain Vimont 44721ed982 Document remote ADB server in README
Refs #2807 <https://github.com/Genymobile/scrcpy/pull/2807>
2021-11-21 22:37:52 +01:00
Romain Vimont 68eaee5bb0 Force adb forward if tunnel host/port is provided
Tunnel host and port are only meaningful in "adb forward" mode.

They indicate where the client must connect to communicate with the
server. In "adb reverse" mode, the client _listens_, it does not
_connect_.

Refs #2807 <https://github.com/Genymobile/scrcpy/pull/2807>
2021-11-21 22:37:52 +01:00
RipleyTom 7bdbde7363 Add options to configure tunnel host and port
In "adb forward" mode, by default, scrcpy connects to localhost:PORT,
where PORT is the local port passed to "adb forward". This assumes that
the tunnel is established on the local host with a local adb server
(which is the common case).

For advanced usage, add --tunnel-host and --tunnel-port to force the
connection to a different destination.

Fixes #2801 <https://github.com/Genymobile/scrcpy/issues/2801>
PR #2807 <https://github.com/Genymobile/scrcpy/pull/2807>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-11-21 22:37:52 +01:00
RipleyTom 52e5181c84 Add function to parse IPv4 addresses
PR #2807 <https://github.com/Genymobile/scrcpy/pull/2807>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-11-21 22:37:37 +01:00
Romain Vimont ba547e3895 Configure feature test macros in meson
Refs #2807 <https://github.com/Genymobile/scrcpy/pull/2807>

Co-authored-by: RipleyTom <RipleyTom@users.noreply.github.com>
2021-11-21 00:20:11 +01:00
Romain Vimont 226f3b2c91 Add missing include config.h 2021-11-21 00:13:25 +01:00
Romain Vimont 2d6a96776c Improve SSH tunnel documentation in README 2021-11-19 21:03:29 +01:00
Romain Vimont 6da6d905c2 Print scrcpy header first
Inconditionnally print the scrcpy version first, without using LOGI().

The log level is configured only after parsing the command line
parameters (it may be changed via -V), and command line parsing logs
should not appear before the scrcpy version.
2021-11-19 09:45:02 +01:00
Romain Vimont b25404ee4b Print help to stdout
The output of -h/--help was printed on stderr, although it is not an
error.

Printing on stdout allows to pipe the result directly:

    scrcpy --help | less

Instead of (in bash):

    scrcpy --help |& less
2021-11-19 08:18:13 +01:00
Romain Vimont 4cfc1cd70a Assert that long options are correctly set 2021-11-19 08:06:23 +01:00
Romain Vimont 2fc80eae2d Simplify adb_tunnel
With the new adb functions, the static adb_tunnel functions become
unnecessary.
2021-11-19 07:25:03 +01:00
Romain Vimont 3fdbd994e0 Privatize low-level adb functions
Only expose the interruptible user-friendly API.
2021-11-19 07:25:03 +01:00
Romain Vimont ce225f019a Use new user-friendly adb API
Replace the adb_exec_*() calls by the new adb functions to simplify.
2021-11-18 22:11:19 +01:00
Romain Vimont b7559744a7 Expose new user-friendly adb functions
Expose interruptible adb functions which return the expected result
directly, without exposing the process (sc_pid) to the caller.
2021-11-18 22:08:15 +01:00
Romain Vimont afb5a5e80f Rename adb functions to adb_exec_*
This paves the way to replace them by more user-friendly functions that
will call them internally.
2021-11-18 21:47:17 +01:00
Romain Vimont 84334cf7db Use sc_intr in file_handler
Replace manual interruption handling by the recent sc_intr mechanism.
2021-11-18 21:33:25 +01:00
Romain Vimont 0ba2686e1d Simplify file_handler
Call the target functions directly.
2021-11-18 19:46:47 +01:00
Romain Vimont 55648d4d64 Improve file_handler readability
Use local variables as short name aliases.
2021-11-18 19:46:40 +01:00
Romain Vimont 13fd693b50 Simplify adb_execute_p()
Only pass the stdout pipe as parameter, scrcpy never writes to stdin or
reads from stderr of an adb process.
2021-11-18 19:43:10 +01:00
Romain Vimont 0426ae885c Make "adb get-serialno" interruptible
All process executions must be interruptible, so that Ctrl+c reacts
immediately.
2021-11-18 19:41:41 +01:00
Romain Vimont ea454e9cee Add interruptible function to read from pipe
This will avoid to block Ctrl+c if the process the pipe is read from
takes too much time.
2021-11-18 19:39:11 +01:00
Romain Vimont cb65531533 Simplify sc_str_truncate()
Use strcspn() to get the prefix length directly.
2021-11-18 19:37:53 +01:00
Romain Vimont 9619ade706 Generalize string trunctation util function
Add an additional argument to let the client pass the possible end
chars.
2021-11-18 18:48:16 +01:00
Romain Vimont f2781a8b6d Expose util function to truncate first line
Move the local implementation from adb functions to the string util
functions.
2021-11-18 18:48:16 +01:00
Romain Vimont 443cb14d6e Assume non-NULL serial in file_handler
The previous commit guarantees to always initialize the serial, so the
file_handle may assume it is never NULL.
2021-11-18 18:48:11 +01:00
Romain Vimont b30c3a429f Always retrieve device serial
This allows to execute all adb commands with the specific -s parameter,
even if it is not provided by the user.

In practice, calling adb without -s works if there is exactly one device
connected. But some adb commands (for example "adb push" on drag & drop)
could be executed after another device is connected, so the actual
device serial must be known.
2021-11-18 18:32:15 +01:00
Romain Vimont 632bd5697b Add missing error handling
If "adb get-serialno" fails, attempting to read from the uninitialized
pipe is incorrect.
2021-11-18 18:32:15 +01:00
Romain Vimont de50846905 Close process on check success
The interruptible version of the function to check process success
(sc_process_check_success_intr()) did not accept a close parameter to
avoid a race condition. But as the result, the processes were not closed
at all.

Add a close parameter, and close the process separately to avoid the
race condition.
2021-11-18 18:31:36 +01:00
Romain Vimont ee93d2aac1 Configure init and cleanup asynchronously
Accessing the settings (like --show-touches) on start should not delay
screen mirroring.

PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
2021-11-18 08:37:32 +01:00
Romain Vimont c29a0bf675 Do not quit on cleanup configuration failure
Cleanup is used for some options like --show-touches to restore the
state on exit.

If the configuration fails, do not crash the whole process. Just log an
error.

PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
2021-11-18 08:37:28 +01:00
Romain Vimont 411bb0d18e Move init and cleanup to a separate method
PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
2021-11-18 08:37:26 +01:00
Romain Vimont cc0902b13c Read/write settings via command on Android >= 12
Before Android 8, executing the "settings" command from a shell was
very slow (~1 second), because it spawned a new app_process to execute
Java code. Therefore, to access settings without performance issues,
scrcpy used private APIs to read from and write to settings.

However, since Android 12, this is not possible anymore, due to
permissions changes.

To make it work again, execute the "settings" command on Android 12 (or
on previous version if the other method failed). This method is faster
than before Android 8 (~100ms).

Fixes #2671 <https://github.com/Genymobile/scrcpy/issues/2671>
Fixes #2788 <https://github.com/Genymobile/scrcpy/issues/2788>
PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
2021-11-18 08:37:22 +01:00
Romain Vimont 48b572c272 Add throwable parameter to Log.w()
When an exception occurs, we might want to log a warning instead of an
error.

PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
2021-11-18 08:37:18 +01:00
Romain Vimont 94feae71f2 Report settings errors via Exceptions
Settings read/write errors were silently ignored. Report them via a
SettingsException so that the caller can handle them.

This allows to log a proper error message, and will also allow to
fallback to a different settings method in case of failure.

PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
2021-11-18 08:37:02 +01:00
Romain Vimont 67170437f1 Wrap settings management into a Settings class
Until now, the code that needed to read/write the Android settings had
to explicitly open and close a ContentProvider.

Wrap these details into a Settings class.

This paves the way to provide an alternative implementation of settings
read/write for Android >= 12.

PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
2021-11-18 08:36:48 +01:00
Romain Vimont 9cb14b5166 Inherit only specific handles on Windows
To be able to communicate with a child process via stdin, stdout and
stderr, the CreateProcess() parameter bInheritHandles must be set to
TRUE. But this causes *all* handles to be inherited, including sockets.

As a result, the server socket was inherited by the process running adb
to execute the server on the device, so it could not be closed properly,
causing other scrcpy instances to fail.

To fix the issue, use an extended API to explicitly set the HANDLEs to
inherit:
 - <https://stackoverflow.com/a/28185363/1987178>
 - <https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873>

Fixes #2779 <https://github.com/Genymobile/scrcpy/issues/2779>
PR #2783 <https://github.com/Genymobile/scrcpy/pull/2783>
2021-11-15 10:13:30 +01:00
Romain Vimont 9cb8766220 Factorize resource release after CreateProcess()
Free the wide characters string in all cases before checking for errors.
2021-11-15 07:49:01 +01:00
Romain Vimont fd4ec784e0 Remove useless assignments on error
Leave the output parameter untouched on error.
2021-11-14 22:53:49 +01:00
Romain Vimont 52cebe1597 Fix Windows sc_pipe function names
The implementation name was incorrect (it was harmless, because they are
not used on Windows).
2021-11-14 22:41:38 +01:00
Romain Vimont 6a27062f48 Stop connection attempts if interrupted
If the interruptor is interrupted, every network call will fail, but the
retry-on-error mechanism must also be stopped.
2021-11-14 15:40:59 +01:00
60 changed files with 1227 additions and 351 deletions

View File

@ -412,21 +412,66 @@ autoadb scrcpy -s '{}'
[AutoAdb]: https://github.com/rom1v/autoadb
#### SSH tunnel
#### Tunnels
To connect to a remote device, it is possible to connect a local `adb` client to
a remote `adb` server (provided they use the same version of the _adb_
protocol):
protocol).
##### Remote ADB server
To connect to a remote ADB server, make the server listen on all interfaces:
```bash
adb kill-server # kill the local adb server on 5037
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
adb kill-server
adb -a nodaemon server start
# keep this open
```
From another terminal:
**Warning: all communications between clients and ADB server are unencrypted.**
Suppose that this server is accessible at 192.168.1.2. Then, from another
terminal, run scrcpy:
```bash
export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037
scrcpy --tunnel-host=192.168.1.2
```
By default, scrcpy uses the local port used for `adb forward` tunnel
establishment (typically `27183`, see `--port`). It is also possible to force a
different tunnel port (it may be useful in more complex situations, when more
redirections are involved):
```
scrcpy --tunnel-port=1234
```
##### SSH tunnel
To communicate with a remote ADB server securely, it is preferable to use a SSH
tunnel.
First, make sure the ADB server is running on the remote computer:
```bash
adb start-server
```
Then, establish a SSH tunnel:
```bash
# local 5038 --> remote 5037
# local 27183 <-- remote 27183
ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer
# keep this open
```
From another terminal, run scrcpy:
```bash
export ADB_SERVER_SOCKET=tcp:localhost:5038
scrcpy
```
@ -434,14 +479,16 @@ To avoid enabling remote port forwarding, you could force a forward connection
instead (notice the `-L` instead of `-R`):
```bash
adb kill-server # kill the local adb server on 5037
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer
# local 5038 --> remote 5037
# local 27183 --> remote 27183
ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer
# keep this open
```
From another terminal:
From another terminal, run scrcpy:
```bash
export ADB_SERVER_SOCKET=tcp:localhost:5038
scrcpy --force-adb-forward
```
@ -683,6 +730,9 @@ of <kbd>Ctrl</kbd>+<kbd>v</kbd> and <kbd>MOD</kbd>+<kbd>v</kbd> so that they
also inject the computer clipboard text as a sequence of key events (the same
way as <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
To disable automatic clipboard synchronization, use
`--no-clipboard-autosync`.
#### Pinch-to-zoom
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.

View File

@ -25,6 +25,7 @@ src = [
'src/server.c',
'src/stream.c',
'src/video_buffer.c',
'src/util/acksync.c',
'src/util/file.c',
'src/util/intr.c',
'src/util/log.c',
@ -39,16 +40,26 @@ src = [
'src/util/tick.c',
]
conf = configuration_data()
if host_machine.system() == 'windows'
src += [
'src/sys/win/file.c',
'src/sys/win/process.c',
]
conf.set('_WIN32_WINNT', '0x0600')
conf.set('WINVER', '0x0600')
else
src += [
'src/sys/unix/file.c',
'src/sys/unix/process.c',
]
conf.set('_POSIX_C_SOURCE', '200809L')
conf.set('_XOPEN_SOURCE', '700')
conf.set('_GNU_SOURCE', true)
if host_machine.system() == 'darwin'
conf.set('_DARWIN_C_SOURCE', true)
endif
endif
v4l2_support = host_machine.system() == 'linux'
@ -128,8 +139,6 @@ if host_machine.system() == 'windows'
dependencies += cc.find_library('ws2_32')
endif
conf = configuration_data()
foreach f : check_functions
if cc.has_function(f)
define = 'HAVE_' + f.underscorify().to_upper()
@ -199,6 +208,7 @@ if get_option('buildtype') == 'debug'
'tests/test_cli.c',
'src/cli.c',
'src/options.c',
'src/util/net.c',
'src/util/str.c',
'src/util/strbuf.c',
'src/util/term.c',

View File

@ -114,6 +114,12 @@ Limit both the width and height of the video to \fIvalue\fR. The other dimension
Default is 0 (unlimited).
.TP
.B \-\-no\-clipboard\-autosync
By default, scrcpy automatically synchronizes the computer clipboard to the device clipboard before injecting Ctrl+v, and the device clipboard to the computer clipboard whenever it changes.
This option disables this automatic synchronization.
.TP
.B \-n, \-\-no\-control
Disable device control (mirror the device in read\-only).
@ -203,6 +209,18 @@ Enable "show touches" on start, restore the initial value on exit.
It only shows physical touches (not clicks from scrcpy).
.TP
.BI "\-\-tunnel\-host " ip
Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward.
Default is localhost.
.TP
.BI "\-\-tunnel\-port " port
Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward.
Default is 0 (not forced): the local port used for establishing the tunnel will be used.
.TP
.BI "\-\-v4l2-sink " /dev/videoN
Output to v4l2loopback device.

View File

@ -7,6 +7,7 @@
#include "util/file.h"
#include "util/log.h"
#include "util/process_intr.h"
#include "util/str.h"
static const char *adb_command;
@ -109,9 +110,9 @@ show_adb_err_msg(enum sc_process_result err, const char *const argv[]) {
free(buf);
}
sc_pid
static sc_pid
adb_execute_p(const char *serial, const char *const adb_cmd[],
size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr) {
size_t len, sc_pipe *pout) {
int i;
sc_pid pid;
@ -132,7 +133,7 @@ adb_execute_p(const char *serial, const char *const adb_cmd[],
memcpy(&argv[i], adb_cmd, len * sizeof(const char *));
argv[len + i] = NULL;
enum sc_process_result r =
sc_process_execute_p(argv, &pid, pin, pout, perr);
sc_process_execute_p(argv, &pid, NULL, pout, NULL);
if (r != SC_PROCESS_SUCCESS) {
show_adb_err_msg(r, argv);
pid = SC_PROCESS_NONE;
@ -144,12 +145,12 @@ adb_execute_p(const char *serial, const char *const adb_cmd[],
sc_pid
adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
return adb_execute_p(serial, adb_cmd, len, NULL, NULL, NULL);
return adb_execute_p(serial, adb_cmd, len, NULL);
}
sc_pid
adb_forward(const char *serial, uint16_t local_port,
const char *device_socket_name) {
static sc_pid
adb_exec_forward(const char *serial, uint16_t local_port,
const char *device_socket_name) {
char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME
sprintf(local, "tcp:%" PRIu16, local_port);
@ -158,17 +159,17 @@ adb_forward(const char *serial, uint16_t local_port,
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
sc_pid
adb_forward_remove(const char *serial, uint16_t local_port) {
static sc_pid
adb_exec_forward_remove(const char *serial, uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT
sprintf(local, "tcp:%" PRIu16, local_port);
const char *const adb_cmd[] = {"forward", "--remove", local};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
sc_pid
adb_reverse(const char *serial, const char *device_socket_name,
uint16_t local_port) {
static sc_pid
adb_exec_reverse(const char *serial, const char *device_socket_name,
uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME
sprintf(local, "tcp:%" PRIu16, local_port);
@ -177,16 +178,16 @@ adb_reverse(const char *serial, const char *device_socket_name,
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
sc_pid
adb_reverse_remove(const char *serial, const char *device_socket_name) {
static sc_pid
adb_exec_reverse_remove(const char *serial, const char *device_socket_name) {
char remote[108 + 14 + 1]; // localabstract:NAME
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name);
const char *const adb_cmd[] = {"reverse", "--remove", remote};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
sc_pid
adb_push(const char *serial, const char *local, const char *remote) {
static sc_pid
adb_exec_push(const char *serial, const char *local, const char *remote) {
#ifdef __WINDOWS__
// Windows will parse the string, so the paths must be quoted
// (see sys/win/command.c)
@ -212,8 +213,8 @@ adb_push(const char *serial, const char *local, const char *remote) {
return pid;
}
sc_pid
adb_install(const char *serial, const char *local) {
static sc_pid
adb_exec_install(const char *serial, const char *local) {
#ifdef __WINDOWS__
// Windows will parse the string, so the local name must be quoted
// (see sys/win/command.c)
@ -233,45 +234,75 @@ adb_install(const char *serial, const char *local) {
return pid;
}
static ssize_t
adb_execute_for_output(const char *serial, const char *const adb_cmd[],
size_t adb_cmd_len, char *buf, size_t buf_len,
const char *name) {
sc_pipe pout;
sc_pid pid = adb_execute_p(serial, adb_cmd, adb_cmd_len, NULL, &pout, NULL);
ssize_t r = sc_pipe_read_all(pout, buf, buf_len);
sc_pipe_close(pout);
if (!sc_process_check_success(pid, name, true)) {
return -1;
}
return r;
static sc_pid
adb_exec_get_serialno(sc_pipe *pout) {
const char *const adb_cmd[] = {"get-serialno"};
return adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), pout);
}
static size_t
truncate_first_line(char *data, size_t len) {
data[len - 1] = '\0';
char *eol = strpbrk(data, "\r\n");
if (eol) {
*eol = '\0';
len = eol - data;
}
return len;
bool
adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port,
const char *device_socket_name) {
sc_pid pid = adb_exec_forward(serial, local_port, device_socket_name);
return sc_process_check_success_intr(intr, pid, "adb forward", true);
}
bool
adb_forward_remove(struct sc_intr *intr, const char *serial,
uint16_t local_port) {
sc_pid pid = adb_exec_forward_remove(serial, local_port);
return sc_process_check_success_intr(intr, pid, "adb forward --remove",
true);
}
bool
adb_reverse(struct sc_intr *intr, const char *serial,
const char *device_socket_name, uint16_t local_port) {
sc_pid pid = adb_exec_reverse(serial, device_socket_name, local_port);
return sc_process_check_success_intr(intr, pid, "adb reverse", true);
}
bool
adb_reverse_remove(struct sc_intr *intr, const char *serial,
const char *device_socket_name) {
sc_pid pid = adb_exec_reverse_remove(serial, device_socket_name);
return sc_process_check_success_intr(intr, pid, "adb reverse --remove",
true);
}
bool
adb_push(struct sc_intr *intr, const char *serial, const char *local,
const char *remote) {
sc_pid pid = adb_exec_push(serial, local, remote);
return sc_process_check_success_intr(intr, pid, "adb push", true);
}
bool
adb_install(struct sc_intr *intr, const char *serial, const char *local) {
sc_pid pid = adb_exec_install(serial, local);
return sc_process_check_success_intr(intr, pid, "adb install", true);
}
char *
adb_get_serialno(void) {
char buf[128];
const char *const adb_cmd[] = {"get-serialno"};
ssize_t r = adb_execute_for_output(NULL, adb_cmd, ARRAY_LEN(adb_cmd),
buf, sizeof(buf), "get-serialno");
if (r <= 0) {
adb_get_serialno(struct sc_intr *intr) {
sc_pipe pout;
sc_pid pid = adb_exec_get_serialno(&pout);
if (pid == SC_PROCESS_NONE) {
LOGE("Could not execute \"adb get-serialno\"");
return NULL;
}
truncate_first_line(buf, r);
char buf[128];
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf));
sc_pipe_close(pout);
bool ok =
sc_process_check_success_intr(intr, pid, "adb get-serialno", true);
if (!ok) {
return NULL;
}
sc_str_truncate(buf, r, " \r\n");
return strdup(buf);
}

View File

@ -6,37 +6,40 @@
#include <stdbool.h>
#include <inttypes.h>
#include "util/process.h"
#include "util/intr.h"
sc_pid
adb_execute(const char *serial, const char *const adb_cmd[], size_t len);
sc_pid
adb_execute_p(const char *serial, const char *const adb_cmd[],
size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr);
sc_pid
adb_forward(const char *serial, uint16_t local_port,
bool
adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port,
const char *device_socket_name);
sc_pid
adb_forward_remove(const char *serial, uint16_t local_port);
bool
adb_forward_remove(struct sc_intr *intr, const char *serial,
uint16_t local_port);
sc_pid
adb_reverse(const char *serial, const char *device_socket_name,
uint16_t local_port);
bool
adb_reverse(struct sc_intr *intr, const char *serial,
const char *device_socket_name, uint16_t local_port);
sc_pid
adb_reverse_remove(const char *serial, const char *device_socket_name);
bool
adb_reverse_remove(struct sc_intr *intr, const char *serial,
const char *device_socket_name);
sc_pid
adb_push(const char *serial, const char *local, const char *remote);
bool
adb_push(struct sc_intr *intr, const char *serial, const char *local,
const char *remote);
sc_pid
adb_install(const char *serial, const char *local);
bool
adb_install(struct sc_intr *intr, const char *serial, const char *local);
// Return the result of "adb get-serialno".
/**
* Execute `adb get-serialno`
*
* Return the result, to be freed by the caller, or NULL on error.
*/
char *
adb_get_serialno(void);
adb_get_serialno(struct sc_intr *intr);
#endif

View File

@ -9,33 +9,6 @@
#define SC_SOCKET_NAME "scrcpy"
static bool
enable_tunnel_reverse(struct sc_intr *intr, const char *serial,
uint16_t local_port) {
sc_pid pid = adb_reverse(serial, SC_SOCKET_NAME, local_port);
return sc_process_check_success_intr(intr, pid, "adb reverse");
}
static bool
disable_tunnel_reverse(struct sc_intr *intr, const char *serial) {
sc_pid pid = adb_reverse_remove(serial, SC_SOCKET_NAME);
return sc_process_check_success_intr(intr, pid, "adb reverse --remove");
}
static bool
enable_tunnel_forward(struct sc_intr *intr, const char *serial,
uint16_t local_port) {
sc_pid pid = adb_forward(serial, local_port, SC_SOCKET_NAME);
return sc_process_check_success_intr(intr, pid, "adb forward");
}
static bool
disable_tunnel_forward(struct sc_intr *intr, const char *serial,
uint16_t local_port) {
sc_pid pid = adb_forward_remove(serial, local_port);
return sc_process_check_success_intr(intr, pid, "adb forward --remove");
}
static bool
listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1);
@ -47,7 +20,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
struct sc_port_range port_range) {
uint16_t port = port_range.first;
for (;;) {
if (!enable_tunnel_reverse(intr, serial, port)) {
if (!adb_reverse(intr, serial, SC_SOCKET_NAME, port)) {
// the command itself failed, it will fail on any port
return false;
}
@ -78,7 +51,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
}
// failure, disable tunnel and try another port
if (!disable_tunnel_reverse(intr, serial)) {
if (!adb_reverse_remove(intr, serial, SC_SOCKET_NAME)) {
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
}
@ -108,7 +81,7 @@ enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel,
uint16_t port = port_range.first;
for (;;) {
if (enable_tunnel_forward(intr, serial, port)) {
if (adb_forward(intr, serial, port, SC_SOCKET_NAME)) {
// success
tunnel->local_port = port;
tunnel->enabled = true;
@ -173,9 +146,9 @@ sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
bool ret;
if (tunnel->forward) {
ret = disable_tunnel_forward(intr, serial, tunnel->local_port);
ret = adb_forward_remove(intr, serial, tunnel->local_port);
} else {
ret = disable_tunnel_reverse(intr, serial);
ret = adb_reverse_remove(intr, serial, SC_SOCKET_NAME);
assert(tunnel->server_socket != SC_SOCKET_NONE);
if (!net_close(tunnel->server_socket)) {

View File

@ -35,7 +35,7 @@ sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id,
hid_event->accessory_id = accessory_id;
hid_event->buffer = buffer;
hid_event->size = buffer_size;
hid_event->delay = 0;
hid_event->ack_to_wait = SC_SEQUENCE_INVALID;
}
void
@ -118,7 +118,10 @@ sc_aoa_open_usb_handle(libusb_device *device, libusb_device_handle **handle) {
}
bool
sc_aoa_init(struct sc_aoa *aoa, const char *serial) {
sc_aoa_init(struct sc_aoa *aoa, const char *serial,
struct sc_acksync *acksync) {
assert(acksync);
cbuf_init(&aoa->queue);
if (!sc_mutex_init(&aoa->mutex)) {
@ -155,6 +158,7 @@ sc_aoa_init(struct sc_aoa *aoa, const char *serial) {
}
aoa->stopped = false;
aoa->acksync = acksync;
return true;
}
@ -332,23 +336,28 @@ run_aoa_thread(void *data) {
assert(non_empty);
(void) non_empty;
assert(event.delay >= 0);
if (event.delay) {
// Wait during the specified delay before injecting the HID event
sc_tick deadline = sc_tick_now() + event.delay;
bool timed_out = false;
while (!aoa->stopped && !timed_out) {
timed_out = !sc_cond_timedwait(&aoa->event_cond, &aoa->mutex,
deadline);
}
if (aoa->stopped) {
sc_mutex_unlock(&aoa->mutex);
uint64_t ack_to_wait = event.ack_to_wait;
sc_mutex_unlock(&aoa->mutex);
if (ack_to_wait != SC_SEQUENCE_INVALID) {
LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait);
// Do not block the loop indefinitely if the ack never comes (it should
// never happen)
sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500);
enum sc_acksync_wait_result result =
sc_acksync_wait(aoa->acksync, ack_to_wait, deadline);
if (result == SC_ACKSYNC_WAIT_TIMEOUT) {
LOGW("Ack not received after 500ms, discarding HID event");
sc_hid_event_destroy(&event);
continue;
} else if (result == SC_ACKSYNC_WAIT_INTR) {
// stopped
sc_hid_event_destroy(&event);
break;
}
}
sc_mutex_unlock(&aoa->mutex);
bool ok = sc_aoa_send_hid_event(aoa, &event);
sc_hid_event_destroy(&event);
if (!ok) {
@ -377,6 +386,8 @@ sc_aoa_stop(struct sc_aoa *aoa) {
aoa->stopped = true;
sc_cond_signal(&aoa->event_cond);
sc_mutex_unlock(&aoa->mutex);
sc_acksync_interrupt(aoa->acksync);
}
void

View File

@ -6,6 +6,7 @@
#include <libusb-1.0/libusb.h>
#include "util/acksync.h"
#include "util/cbuf.h"
#include "util/thread.h"
#include "util/tick.h"
@ -14,7 +15,7 @@ struct sc_hid_event {
uint16_t accessory_id;
unsigned char *buffer;
uint16_t size;
sc_tick delay;
uint64_t ack_to_wait;
};
// Takes ownership of buffer
@ -36,10 +37,12 @@ struct sc_aoa {
sc_cond event_cond;
bool stopped;
struct sc_hid_event_queue queue;
struct sc_acksync *acksync;
};
bool
sc_aoa_init(struct sc_aoa *aoa, const char *serial);
sc_aoa_init(struct sc_aoa *aoa, const char *serial, struct sc_acksync *acksync);
void
sc_aoa_destroy(struct sc_aoa *aoa);

View File

@ -9,6 +9,7 @@
#include "options.h"
#include "util/log.h"
#include "util/net.h"
#include "util/str.h"
#include "util/strbuf.h"
#include "util/term.h"
@ -46,6 +47,9 @@
#define OPT_V4L2_SINK 1027
#define OPT_DISPLAY_BUFFER 1028
#define OPT_V4L2_BUFFER 1029
#define OPT_TUNNEL_HOST 1030
#define OPT_TUNNEL_PORT 1031
#define OPT_NO_CLIPBOARD_AUTOSYNC 1032
struct sc_option {
char shortopt;
@ -205,6 +209,15 @@ static const struct sc_option options[] = {
"is preserved.\n"
"Default is 0 (unlimited).",
},
{
.longopt_id = OPT_NO_CLIPBOARD_AUTOSYNC,
.longopt = "no-clipboard-autosync",
.text = "By default, scrcpy automatically synchronizes the computer "
"clipboard to the device clipboard before injecting Ctrl+v, "
"and the device clipboard to the computer clipboard whenever "
"it changes.\n"
"This option disables this automatic synchronization."
},
{
.shortopt = 'n',
.longopt = "no-control",
@ -330,6 +343,25 @@ static const struct sc_option options[] = {
"on exit.\n"
"It only shows physical touches (not clicks from scrcpy).",
},
{
.longopt_id = OPT_TUNNEL_HOST,
.longopt = "tunnel-host",
.argdesc = "ip",
.text = "Set the IP address of the adb tunnel to reach the scrcpy "
"server. This option automatically enables "
"--force-adb-forward.\n"
"Default is localhost.",
},
{
.longopt_id = OPT_TUNNEL_PORT,
.longopt = "tunnel-port",
.argdesc = "port",
.text = "Set the TCP port of the adb tunnel to reach the scrcpy "
"server. This option automatically enables "
"--force-adb-forward.\n"
"Default is 0 (not forced): the local port used for "
"establishing the tunnel will be used.",
},
#ifdef HAVE_V4L2
{
.longopt_id = OPT_V4L2_SINK,
@ -569,6 +601,10 @@ sc_getopt_adapter_create_longopts(void) {
size_t out_idx = 0;
for (size_t i = 0; i < ARRAY_LEN(options); ++i) {
const struct sc_option *in = &options[i];
// If longopt_id is set, then longopt must be set
assert(!in->longopt_id || in->longopt);
if (!in->longopt) {
// The longopts array must only contain long options
continue;
@ -671,12 +707,12 @@ print_option_usage_header(const struct sc_option *opt) {
}
}
fprintf(stderr, "\n %s\n", buf.s);
printf("\n %s\n", buf.s);
free(buf.s);
return;
error:
fprintf(stderr, "<ERROR>\n");
printf("<ERROR>\n");
}
static void
@ -692,11 +728,11 @@ print_option_usage(const struct sc_option *opt, unsigned cols) {
char *text = sc_str_wrap_lines(opt->text, cols, 8);
if (!text) {
fprintf(stderr, "<ERROR>\n");
printf("<ERROR>\n");
return;
}
fprintf(stderr, "%s\n", text);
printf("%s\n", text);
free(text);
}
@ -707,11 +743,11 @@ print_shortcuts_intro(unsigned cols) {
"(left) Alt or (left) Super, but it can be configured by "
"--shortcut-mod (see above).", cols, 4);
if (!intro) {
fprintf(stderr, "<ERROR>\n");
printf("<ERROR>\n");
return;
}
fprintf(stderr, "%s\n", intro);
printf("%s\n", intro);
free(intro);
}
@ -721,21 +757,21 @@ print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) {
assert(shortcut->shortcuts[0]); // At least one shortcut
assert(shortcut->text);
fprintf(stderr, "\n");
printf("\n");
unsigned i = 0;
while (shortcut->shortcuts[i]) {
fprintf(stderr, " %s\n", shortcut->shortcuts[i]);
printf(" %s\n", shortcut->shortcuts[i]);
++i;
};
char *text = sc_str_wrap_lines(shortcut->text, cols, 8);
if (!text) {
fprintf(stderr, "<ERROR>\n");
printf("<ERROR>\n");
return;
}
fprintf(stderr, "%s\n", text);
printf("%s\n", text);
free(text);
}
@ -759,14 +795,14 @@ scrcpy_print_usage(const char *arg0) {
}
}
fprintf(stderr, "Usage: %s [options]\n\n"
"Options:\n", arg0);
printf("Usage: %s [options]\n\n"
"Options:\n", arg0);
for (size_t i = 0; i < ARRAY_LEN(options); ++i) {
print_option_usage(&options[i], cols);
}
// Print shortcuts section
fprintf(stderr, "\nShortcuts:\n\n");
printf("\nShortcuts:\n\n");
print_shortcuts_intro(cols);
for (size_t i = 0; i < ARRAY_LEN(shortcuts); ++i) {
print_shortcut(&shortcuts[i], cols);
@ -1123,6 +1159,21 @@ parse_record_format(const char *optarg, enum sc_record_format *format) {
return false;
}
static bool
parse_ip(const char *optarg, uint32_t *ipv4) {
return net_parse_ipv4(optarg, ipv4);
}
static bool
parse_port(const char *optarg, uint16_t *port) {
long value;
if (!parse_integer_arg(optarg, &value, false, 0, 0xFFFF, "port")) {
return false;
}
*port = (uint16_t) value;
return true;
}
static enum sc_record_format
guess_record_format(const char *filename) {
size_t len = strlen(filename);
@ -1195,6 +1246,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
break;
case OPT_TUNNEL_HOST:
if (!parse_ip(optarg, &opts->tunnel_host)) {
return false;
}
break;
case OPT_TUNNEL_PORT:
if (!parse_port(optarg, &opts->tunnel_port)) {
return false;
}
break;
case 'n':
opts->control = false;
break;
@ -1313,6 +1374,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
break;
case OPT_NO_CLIPBOARD_AUTOSYNC:
opts->clipboard_autosync = false;
break;
#ifdef HAVE_V4L2
case OPT_V4L2_SINK:
opts->v4l2_device = optarg;
@ -1354,6 +1418,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
#endif
if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) {
LOGI("Tunnel host/port is set, "
"--force-adb-forward automatically enabled.");
opts->force_adb_forward = true;
}
int index = optind;
if (index < argc) {
LOGE("Unexpected additional argument: %s", argv[index]);

View File

@ -1,16 +1,17 @@
#ifndef COMPAT_H
#define COMPAT_H
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#define _GNU_SOURCE
#ifdef __APPLE__
# define _DARWIN_C_SOURCE
#endif
#include "config.h"
#include <libavformat/version.h>
#include <SDL2/SDL_version.h>
#ifndef __WIN32
# define PRIu64_ PRIu64
#else
# define PRIu64_ "I64u" // Windows...
#endif
// In ffmpeg/doc/APIchanges:
// 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h
// Deprecate use of av_register_input_format(), av_register_output_format(),

View File

@ -118,11 +118,12 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
buf[1] = msg->inject_keycode.action;
return 2;
case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
buf[1] = !!msg->set_clipboard.paste;
buffer_write64be(&buf[1], msg->set_clipboard.sequence);
buf[9] = !!msg->set_clipboard.paste;
size_t len = write_string(msg->set_clipboard.text,
CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH,
&buf[2]);
return 2 + len;
&buf[10]);
return 10 + len;
}
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode;
@ -170,11 +171,6 @@ control_msg_log(const struct control_msg *msg) {
(long) msg->inject_touch_event.buttons);
} else {
// numeric pointer id
#ifndef __WIN32
# define PRIu64_ PRIu64
#else
# define PRIu64_ "I64u" // Windows...
#endif
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
PRIi32 " pressure=%g buttons=%06lx",
id,
@ -199,7 +195,8 @@ control_msg_log(const struct control_msg *msg) {
KEYEVENT_ACTION_LABEL(msg->inject_keycode.action));
break;
case CONTROL_MSG_TYPE_SET_CLIPBOARD:
LOG_CMSG("clipboard %s \"%s\"",
LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"",
msg->set_clipboard.sequence,
msg->set_clipboard.paste ? "paste" : "copy",
msg->set_clipboard.text);
break;

View File

@ -70,6 +70,7 @@ struct control_msg {
// screen may only be turned on on ACTION_DOWN
} back_or_screen_on;
struct {
uint64_t sequence;
char *text; // owned, to be freed by free()
bool paste;
} set_clipboard;

View File

@ -5,10 +5,11 @@
#include "util/log.h"
bool
controller_init(struct controller *controller, sc_socket control_socket) {
controller_init(struct controller *controller, sc_socket control_socket,
struct sc_acksync *acksync) {
cbuf_init(&controller->queue);
bool ok = receiver_init(&controller->receiver, control_socket);
bool ok = receiver_init(&controller->receiver, control_socket, acksync);
if (!ok) {
return false;
}

View File

@ -7,6 +7,7 @@
#include "control_msg.h"
#include "receiver.h"
#include "util/acksync.h"
#include "util/cbuf.h"
#include "util/net.h"
#include "util/thread.h"
@ -24,7 +25,8 @@ struct controller {
};
bool
controller_init(struct controller *controller, sc_socket control_socket);
controller_init(struct controller *controller, sc_socket control_socket,
struct sc_acksync *acksync);
void
controller_destroy(struct controller *controller);

View File

@ -1,5 +1,6 @@
#include "device_msg.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@ -34,6 +35,11 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
msg->clipboard.text = text;
return 5 + clipboard_len;
}
case DEVICE_MSG_TYPE_ACK_CLIPBOARD: {
uint64_t sequence = buffer_read64be(&buf[1]);
msg->ack_clipboard.sequence = sequence;
return 9;
}
default:
LOGW("Unknown device message type: %d", (int) msg->type);
return -1; // error, we cannot recover

View File

@ -13,6 +13,7 @@
enum device_msg_type {
DEVICE_MSG_TYPE_CLIPBOARD,
DEVICE_MSG_TYPE_ACK_CLIPBOARD,
};
struct device_msg {
@ -21,6 +22,9 @@ struct device_msg {
struct {
char *text; // owned, to be freed by free()
} clipboard;
struct {
uint64_t sequence;
} ack_clipboard;
};
};

View File

@ -5,6 +5,7 @@
#include "adb.h"
#include "util/log.h"
#include "util/process_intr.h"
#define DEFAULT_PUSH_TARGET "/sdcard/Download/"
@ -16,6 +17,7 @@ file_handler_request_destroy(struct file_handler_request *req) {
bool
file_handler_init(struct file_handler *file_handler, const char *serial,
const char *push_target) {
assert(serial);
cbuf_init(&file_handler->queue);
@ -30,23 +32,26 @@ file_handler_init(struct file_handler *file_handler, const char *serial,
return false;
}
if (serial) {
file_handler->serial = strdup(serial);
if (!file_handler->serial) {
LOGW("Could not strdup serial");
sc_cond_destroy(&file_handler->event_cond);
sc_mutex_destroy(&file_handler->mutex);
return false;
}
} else {
file_handler->serial = NULL;
ok = sc_intr_init(&file_handler->intr);
if (!ok) {
LOGE("Could not create intr");
sc_cond_destroy(&file_handler->event_cond);
sc_mutex_destroy(&file_handler->mutex);
}
file_handler->serial = strdup(serial);
if (!file_handler->serial) {
LOGE("Could not strdup serial");
sc_intr_destroy(&file_handler->intr);
sc_cond_destroy(&file_handler->event_cond);
sc_mutex_destroy(&file_handler->mutex);
return false;
}
// lazy initialization
file_handler->initialized = false;
file_handler->stopped = false;
file_handler->current_process = SC_PROCESS_NONE;
file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET;
@ -57,6 +62,7 @@ void
file_handler_destroy(struct file_handler *file_handler) {
sc_cond_destroy(&file_handler->event_cond);
sc_mutex_destroy(&file_handler->mutex);
sc_intr_destroy(&file_handler->intr);
free(file_handler->serial);
struct file_handler_request req;
@ -65,16 +71,6 @@ file_handler_destroy(struct file_handler *file_handler) {
}
}
static sc_pid
install_apk(const char *serial, const char *file) {
return adb_install(serial, file);
}
static sc_pid
push_file(const char *serial, const char *file, const char *push_target) {
return adb_push(serial, file, push_target);
}
bool
file_handler_request(struct file_handler *file_handler,
file_handler_action_t action, char *file) {
@ -106,10 +102,16 @@ file_handler_request(struct file_handler *file_handler,
static int
run_file_handler(void *data) {
struct file_handler *file_handler = data;
struct sc_intr *intr = &file_handler->intr;
const char *serial = file_handler->serial;
assert(serial);
const char *push_target = file_handler->push_target;
assert(push_target);
for (;;) {
sc_mutex_lock(&file_handler->mutex);
file_handler->current_process = SC_PROCESS_NONE;
while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) {
sc_cond_wait(&file_handler->event_cond, &file_handler->mutex);
}
@ -122,43 +124,26 @@ run_file_handler(void *data) {
bool non_empty = cbuf_take(&file_handler->queue, &req);
assert(non_empty);
(void) non_empty;
sc_pid pid;
if (req.action == ACTION_INSTALL_APK) {
LOGI("Installing %s...", req.file);
pid = install_apk(file_handler->serial, req.file);
} else {
LOGI("Pushing %s...", req.file);
pid = push_file(file_handler->serial, req.file,
file_handler->push_target);
}
file_handler->current_process = pid;
sc_mutex_unlock(&file_handler->mutex);
if (req.action == ACTION_INSTALL_APK) {
if (sc_process_check_success(pid, "adb install", false)) {
LOGI("Installing %s...", req.file);
bool ok = adb_install(intr, serial, req.file);
if (ok) {
LOGI("%s successfully installed", req.file);
} else {
LOGE("Failed to install %s", req.file);
}
} else {
if (sc_process_check_success(pid, "adb push", false)) {
LOGI("%s successfully pushed to %s", req.file,
file_handler->push_target);
LOGI("Pushing %s...", req.file);
bool ok = adb_push(intr, serial, req.file, push_target);
if (ok) {
LOGI("%s successfully pushed to %s", req.file, push_target);
} else {
LOGE("Failed to push %s to %s", req.file,
file_handler->push_target);
LOGE("Failed to push %s to %s", req.file, push_target);
}
}
sc_mutex_lock(&file_handler->mutex);
// Close the process (it is necessarily already terminated)
// Execute this call with mutex locked to avoid race conditions with
// file_handler_stop()
sc_process_close(file_handler->current_process);
file_handler->current_process = SC_PROCESS_NONE;
sc_mutex_unlock(&file_handler->mutex);
file_handler_request_destroy(&req);
}
return 0;
@ -183,11 +168,7 @@ file_handler_stop(struct file_handler *file_handler) {
sc_mutex_lock(&file_handler->mutex);
file_handler->stopped = true;
sc_cond_signal(&file_handler->event_cond);
if (file_handler->current_process != SC_PROCESS_NONE) {
if (!sc_process_terminate(file_handler->current_process)) {
LOGW("Could not terminate push/install process");
}
}
sc_intr_interrupt(&file_handler->intr);
sc_mutex_unlock(&file_handler->mutex);
}

View File

@ -8,6 +8,7 @@
#include "adb.h"
#include "util/cbuf.h"
#include "util/thread.h"
#include "util/intr.h"
typedef enum {
ACTION_INSTALL_APK,
@ -29,8 +30,9 @@ struct file_handler {
sc_cond event_cond;
bool stopped;
bool initialized;
sc_pid current_process;
struct file_handler_request_queue queue;
struct sc_intr intr;
};
bool

View File

@ -278,7 +278,8 @@ push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t sdl_mod) {
static void
sc_key_processor_process_key(struct sc_key_processor *kp,
const SDL_KeyboardEvent *event) {
const SDL_KeyboardEvent *event,
uint64_t ack_to_wait) {
if (event->repeat) {
// In USB HID protocol, key repeat is handled by the host (Android), so
// just ignore key repeat here.
@ -298,16 +299,12 @@ sc_key_processor_process_key(struct sc_key_processor *kp,
}
}
SDL_Keycode keycode = event->keysym.sym;
bool down = event->type == SDL_KEYDOWN;
bool ctrl = event->keysym.mod & KMOD_CTRL;
bool shift = event->keysym.mod & KMOD_SHIFT;
if (ctrl && !shift && keycode == SDLK_v && down) {
if (ack_to_wait) {
// Ctrl+v is pressed, so clipboard synchronization has been
// requested. Wait a bit so that the clipboard is set before
// injecting Ctrl+v via HID, otherwise it would paste the old
// clipboard content.
hid_event.delay = SC_TICK_FROM_MS(5);
// requested. Wait until clipboard synchronization is acknowledged
// by the server, otherwise it could paste the old clipboard
// content.
hid_event.ack_to_wait = ack_to_wait;
}
if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) {
@ -348,6 +345,10 @@ sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa) {
.process_text = sc_key_processor_process_text,
};
// Clipboard synchronization is requested over the control socket, while HID
// events are sent over AOA, so it must wait for clipboard synchronization
// to be acknowledged by the device before injecting Ctrl+v.
kb->key_processor.async_paste = true;
kb->key_processor.ops = &ops;
return true;

View File

@ -66,6 +66,7 @@ input_manager_init(struct input_manager *im, struct controller *controller,
im->control = options->control;
im->forward_all_clicks = options->forward_all_clicks;
im->legacy_paste = options->legacy_paste;
im->clipboard_autosync = options->clipboard_autosync;
const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods;
assert(shortcut_mods->count);
@ -82,6 +83,8 @@ input_manager_init(struct input_manager *im, struct controller *controller,
im->last_keycode = SDLK_UNKNOWN;
im->last_mod = 0;
im->key_repeat = 0;
im->next_sequence = 1; // 0 is reserved for SC_SEQUENCE_INVALID
}
static void
@ -208,35 +211,35 @@ collapse_panels(struct controller *controller) {
}
}
static void
set_device_clipboard(struct controller *controller, bool paste) {
static bool
set_device_clipboard(struct controller *controller, bool paste,
uint64_t sequence) {
char *text = SDL_GetClipboardText();
if (!text) {
LOGW("Could not get clipboard text: %s", SDL_GetError());
return;
}
if (!*text) {
// empty text
SDL_free(text);
return;
return false;
}
char *text_dup = strdup(text);
SDL_free(text);
if (!text_dup) {
LOGW("Could not strdup input text");
return;
return false;
}
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD;
msg.set_clipboard.sequence = sequence;
msg.set_clipboard.text = text_dup;
msg.set_clipboard.paste = paste;
if (!controller_push_msg(controller, &msg)) {
free(text_dup);
LOGW("Could not request 'set device clipboard'");
return false;
}
return true;
}
static void
@ -462,8 +465,10 @@ input_manager_process_key(struct input_manager *im,
// inject the text as input events
clipboard_paste(controller);
} else {
// store the text in the device clipboard and paste
set_device_clipboard(controller, true);
// store the text in the device clipboard and paste,
// without requesting an acknowledgment
set_device_clipboard(controller, true,
SC_SEQUENCE_INVALID);
}
}
return;
@ -512,18 +517,36 @@ input_manager_process_key(struct input_manager *im,
return;
}
if (ctrl && !shift && keycode == SDLK_v && down && !repeat) {
uint64_t ack_to_wait = SC_SEQUENCE_INVALID;
bool is_ctrl_v = ctrl && !shift && keycode == SDLK_v && down && !repeat;
if (im->clipboard_autosync && is_ctrl_v) {
if (im->legacy_paste) {
// inject the text as input events
clipboard_paste(controller);
return;
}
// Request an acknowledgement only if necessary
uint64_t sequence = im->kp->async_paste ? im->next_sequence
: SC_SEQUENCE_INVALID;
// Synchronize the computer clipboard to the device clipboard before
// sending Ctrl+v, to allow seamless copy-paste.
set_device_clipboard(controller, false);
bool ok = set_device_clipboard(controller, false, sequence);
if (!ok) {
LOGW("Clipboard could not be synchronized, Ctrl+v not injected");
return;
}
if (im->kp->async_paste) {
// The key processor must wait for this ack before injecting Ctrl+v
ack_to_wait = sequence;
// Increment only when the request succeeded
++im->next_sequence;
}
}
im->kp->ops->process_key(im->kp, event);
im->kp->ops->process_key(im->kp, event, ack_to_wait);
}
static void

View File

@ -24,6 +24,7 @@ struct input_manager {
bool control;
bool forward_all_clicks;
bool legacy_paste;
bool clipboard_autosync;
struct {
unsigned data[SC_MAX_SHORTCUT_MODS];
@ -38,6 +39,8 @@ struct input_manager {
unsigned key_repeat;
SDL_Keycode last_keycode;
uint16_t last_mod;
uint64_t next_sequence; // used for request acknowledgements
};
void

View File

@ -188,7 +188,13 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
static void
sc_key_processor_process_key(struct sc_key_processor *kp,
const SDL_KeyboardEvent *event) {
const SDL_KeyboardEvent *event,
uint64_t ack_to_wait) {
// The device clipboard synchronization and the key event messages are
// serialized, there is nothing special to do to ensure that the clipboard
// is set before injecting Ctrl+v.
(void) ack_to_wait;
struct sc_keyboard_inject *ki = DOWNCAST(kp);
if (event->repeat) {
@ -250,5 +256,7 @@ sc_keyboard_inject_init(struct sc_keyboard_inject *ki,
.process_text = sc_key_processor_process_text,
};
// Key injection and clipboard synchronization are serialized
ki->key_processor.async_paste = false;
ki->key_processor.ops = &ops;
}

View File

@ -47,6 +47,9 @@ main(int argc, char *argv[]) {
setbuf(stderr, NULL);
#endif
printf("scrcpy " SCRCPY_VERSION
" <https://github.com/Genymobile/scrcpy>\n");
struct scrcpy_cli_args args = {
.opts = scrcpy_options_default,
.help = false,
@ -73,8 +76,6 @@ main(int argc, char *argv[]) {
return 0;
}
LOGI("scrcpy " SCRCPY_VERSION " <https://github.com/Genymobile/scrcpy>");
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
av_register_all();
#endif

View File

@ -19,6 +19,8 @@ const struct scrcpy_options scrcpy_options_default = {
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST,
.last = DEFAULT_LOCAL_PORT_RANGE_LAST,
},
.tunnel_host = 0,
.tunnel_port = 0,
.shortcut_mods = {
.data = {SC_MOD_LALT, SC_MOD_LSUPER},
.count = 2,
@ -51,4 +53,5 @@ const struct scrcpy_options scrcpy_options_default = {
.forward_all_clicks = false,
.legacy_paste = false,
.power_off_on_close = false,
.clipboard_autosync = true,
};

View File

@ -77,6 +77,8 @@ struct scrcpy_options {
enum sc_record_format record_format;
enum sc_keyboard_input_mode keyboard_input_mode;
struct sc_port_range port_range;
uint32_t tunnel_host;
uint16_t tunnel_port;
struct sc_shortcut_mods shortcut_mods;
uint16_t max_size;
uint32_t bit_rate;
@ -106,6 +108,7 @@ struct scrcpy_options {
bool forward_all_clicks;
bool legacy_paste;
bool power_off_on_close;
bool clipboard_autosync;
};
extern const struct scrcpy_options scrcpy_options_default;

View File

@ -7,12 +7,16 @@
#include "util/log.h"
bool
receiver_init(struct receiver *receiver, sc_socket control_socket) {
receiver_init(struct receiver *receiver, sc_socket control_socket,
struct sc_acksync *acksync) {
bool ok = sc_mutex_init(&receiver->mutex);
if (!ok) {
return false;
}
receiver->control_socket = control_socket;
receiver->acksync = acksync;
return true;
}
@ -22,7 +26,7 @@ receiver_destroy(struct receiver *receiver) {
}
static void
process_msg(struct device_msg *msg) {
process_msg(struct receiver *receiver, struct device_msg *msg) {
switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: {
char *current = SDL_GetClipboardText();
@ -37,11 +41,17 @@ process_msg(struct device_msg *msg) {
SDL_SetClipboardText(msg->clipboard.text);
break;
}
case DEVICE_MSG_TYPE_ACK_CLIPBOARD:
assert(receiver->acksync);
LOGD("Ack device clipboard sequence=%" PRIu64_,
msg->ack_clipboard.sequence);
sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence);
break;
}
}
static ssize_t
process_msgs(const unsigned char *buf, size_t len) {
process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
size_t head = 0;
for (;;) {
struct device_msg msg;
@ -53,7 +63,7 @@ process_msgs(const unsigned char *buf, size_t len) {
return head;
}
process_msg(&msg);
process_msg(receiver, &msg);
device_msg_destroy(&msg);
head += r;
@ -81,7 +91,7 @@ run_receiver(void *data) {
}
head += r;
ssize_t consumed = process_msgs(buf, head);
ssize_t consumed = process_msgs(receiver, buf, head);
if (consumed == -1) {
// an error occurred
break;

View File

@ -5,6 +5,7 @@
#include <stdbool.h>
#include "util/acksync.h"
#include "util/net.h"
#include "util/thread.h"
@ -14,10 +15,13 @@ struct receiver {
sc_socket control_socket;
sc_thread thread;
sc_mutex mutex;
struct sc_acksync *acksync;
};
bool
receiver_init(struct receiver *receiver, sc_socket control_socket);
receiver_init(struct receiver *receiver, sc_socket control_socket,
struct sc_acksync *acksync);
void
receiver_destroy(struct receiver *receiver);

View File

@ -27,6 +27,7 @@
#include "screen.h"
#include "server.h"
#include "stream.h"
#include "util/acksync.h"
#include "util/log.h"
#include "util/net.h"
#ifdef HAVE_V4L2
@ -46,6 +47,8 @@ struct scrcpy {
struct file_handler file_handler;
#ifdef HAVE_AOA_HID
struct sc_aoa aoa;
// sequence/ack helper to synchronize clipboard and Ctrl+v via HID
struct sc_acksync acksync;
#endif
union {
struct sc_keyboard_inject keyboard_inject;
@ -340,11 +343,15 @@ scrcpy(struct scrcpy_options *options) {
bool controller_started = false;
bool screen_initialized = false;
struct sc_acksync *acksync = NULL;
struct sc_server_params params = {
.serial = options->serial,
.log_level = options->log_level,
.crop = options->crop,
.port_range = options->port_range,
.tunnel_host = options->tunnel_host,
.tunnel_port = options->tunnel_port,
.max_size = options->max_size,
.bit_rate = options->bit_rate,
.max_fps = options->max_fps,
@ -357,6 +364,7 @@ scrcpy(struct scrcpy_options *options) {
.encoder_name = options->encoder_name,
.force_adb_forward = options->force_adb_forward,
.power_off_on_close = options->power_off_on_close,
.clipboard_autosync = options->clipboard_autosync,
};
static const struct sc_server_callbacks cbs = {
@ -394,8 +402,11 @@ scrcpy(struct scrcpy_options *options) {
// It is necessarily initialized here, since the device is connected
struct sc_server_info *info = &s->server.info;
const char *serial = s->server.params.serial;
assert(serial);
if (options->display && options->control) {
if (!file_handler_init(&s->file_handler, options->serial,
if (!file_handler_init(&s->file_handler, serial,
options->push_target)) {
goto end;
}
@ -440,7 +451,18 @@ scrcpy(struct scrcpy_options *options) {
}
if (options->control) {
if (!controller_init(&s->controller, s->server.control_socket)) {
#ifdef HAVE_AOA_HID
if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) {
bool ok = sc_acksync_init(&s->acksync);
if (!ok) {
goto end;
}
acksync = &s->acksync;
}
#endif
if (!controller_init(&s->controller, s->server.control_socket,
acksync)) {
goto end;
}
controller_initialized = true;
@ -516,21 +538,7 @@ scrcpy(struct scrcpy_options *options) {
#ifdef HAVE_AOA_HID
bool aoa_hid_ok = false;
char *serialno = NULL;
const char *serial = options->serial;
if (!serial) {
serialno = adb_get_serialno();
if (!serialno) {
LOGE("Could not get device serial");
goto aoa_hid_end;
}
serial = serialno;
LOGI("Device serial: %s", serial);
}
bool ok = sc_aoa_init(&s->aoa, serial);
free(serialno);
bool ok = sc_aoa_init(&s->aoa, serial, acksync);
if (!ok) {
goto aoa_hid_end;
}
@ -595,6 +603,9 @@ end:
sc_hid_keyboard_destroy(&s->keyboard_hid);
sc_aoa_stop(&s->aoa);
}
if (acksync) {
sc_acksync_destroy(acksync);
}
#endif
if (controller_started) {
controller_stop(&s->controller);

View File

@ -112,9 +112,9 @@ push_server(struct sc_intr *intr, const char *serial) {
free(server_path);
return false;
}
sc_pid pid = adb_push(serial, server_path, SC_DEVICE_SERVER_PATH);
bool ok = adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH);
free(server_path);
return sc_process_check_success_intr(intr, pid, "adb push");
return ok;
}
static const char *
@ -186,6 +186,7 @@ execute_server(struct sc_server *server,
params->codec_options ? params->codec_options : "-",
params->encoder_name ? params->encoder_name : "-",
params->power_off_on_close ? "true" : "false",
params->clipboard_autosync ? "true" : "false",
};
#ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port "
@ -202,8 +203,9 @@ execute_server(struct sc_server *server,
}
static bool
connect_and_read_byte(struct sc_intr *intr, sc_socket socket, uint16_t port) {
bool ok = net_connect_intr(intr, socket, IPV4_LOCALHOST, port);
connect_and_read_byte(struct sc_intr *intr, sc_socket socket,
uint32_t tunnel_host, uint16_t tunnel_port) {
bool ok = net_connect_intr(intr, socket, tunnel_host, tunnel_port);
if (!ok) {
return false;
}
@ -220,13 +222,13 @@ connect_and_read_byte(struct sc_intr *intr, sc_socket socket, uint16_t port) {
}
static sc_socket
connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay) {
uint16_t port = server->tunnel.local_port;
connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay,
uint32_t host, uint16_t port) {
do {
LOGD("Remaining connection attempts: %d", (int) attempts);
sc_socket socket = net_socket();
if (socket != SC_SOCKET_NONE) {
bool ok = connect_and_read_byte(&server->intr, socket, port);
bool ok = connect_and_read_byte(&server->intr, socket, host, port);
if (ok) {
// it worked!
return socket;
@ -234,6 +236,12 @@ connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay) {
net_close(socket);
}
if (sc_intr_is_interrupted(&server->intr)) {
// Stop immediately
break;
}
if (attempts) {
sc_mutex_lock(&server->mutex);
sc_tick deadline = sc_tick_now() + delay;
@ -346,9 +354,20 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
goto fail;
}
} else {
uint32_t tunnel_host = server->params.tunnel_host;
if (!tunnel_host) {
tunnel_host = IPV4_LOCALHOST;
}
uint16_t tunnel_port = server->params.tunnel_port;
if (!tunnel_port) {
tunnel_port = tunnel->local_port;
}
uint32_t attempts = 100;
sc_tick delay = SC_TICK_FROM_MS(100);
video_socket = connect_to_server(server, attempts, delay);
video_socket = connect_to_server(server, attempts, delay, tunnel_host,
tunnel_port);
if (video_socket == SC_SOCKET_NONE) {
goto fail;
}
@ -358,8 +377,8 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
if (control_socket == SC_SOCKET_NONE) {
goto fail;
}
bool ok = net_connect_intr(&server->intr, control_socket,
IPV4_LOCALHOST, tunnel->local_port);
bool ok = net_connect_intr(&server->intr, control_socket, tunnel_host,
tunnel_port);
if (!ok) {
goto fail;
}
@ -416,12 +435,36 @@ sc_server_on_terminated(void *userdata) {
LOGD("Server terminated");
}
static bool
sc_server_fill_serial(struct sc_server *server) {
// Retrieve the actual device immediately if not provided, so that all
// future adb commands are executed for this specific device, even if other
// devices are connected afterwards (without "more than one
// device/emulator" error)
if (!server->params.serial) {
// The serial is owned by sc_server_params, and will be freed on destroy
server->params.serial = adb_get_serialno(&server->intr);
if (!server->params.serial) {
LOGE("Could not get device serial");
return false;
}
}
return true;
}
static int
run_server(void *data) {
struct sc_server *server = data;
if (!sc_server_fill_serial(server)) {
goto error_connection_failed;
}
const struct sc_server_params *params = &server->params;
LOGD("Device serial: %s", params->serial);
bool ok = push_server(&server->intr, params->serial);
if (!ok) {
goto error_connection_failed;

View File

@ -29,6 +29,8 @@ struct sc_server_params {
const char *codec_options;
const char *encoder_name;
struct sc_port_range port_range;
uint32_t tunnel_host;
uint16_t tunnel_port;
uint16_t max_size;
uint32_t bit_rate;
uint16_t max_fps;
@ -39,6 +41,7 @@ struct sc_server_params {
bool stay_awake;
bool force_adb_forward;
bool power_off_on_close;
bool clipboard_autosync;
};
struct sc_server {

View File

@ -1,5 +1,7 @@
#include "util/process.h"
#include <processthreadsapi.h>
#include <assert.h>
#include "util/log.h"
@ -26,6 +28,9 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle,
HANDLE *pin, HANDLE *pout, HANDLE *perr) {
enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC;
// Add 1 per non-NULL pointer
unsigned handle_count = !!pin + !!pout + !!perr;
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
@ -65,45 +70,94 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle,
}
}
STARTUPINFOW si;
STARTUPINFOEXW si;
PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
if (pin || pout || perr) {
si.dwFlags = STARTF_USESTDHANDLES;
si.StartupInfo.cb = sizeof(si);
HANDLE handles[3];
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL;
if (handle_count) {
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
if (pin) {
si.hStdInput = stdin_read_handle;
si.StartupInfo.hStdInput = stdin_read_handle;
}
if (pout) {
si.hStdOutput = stdout_write_handle;
si.StartupInfo.hStdOutput = stdout_write_handle;
}
if (perr) {
si.hStdError = stderr_write_handle;
si.StartupInfo.hStdError = stderr_write_handle;
}
SIZE_T size;
// Call it once to know the required buffer size
BOOL ok =
InitializeProcThreadAttributeList(NULL, 1, 0, &size)
|| GetLastError() == ERROR_INSUFFICIENT_BUFFER;
if (!ok) {
goto error_close_stderr;
}
lpAttributeList = malloc(size);
if (!lpAttributeList) {
goto error_close_stderr;
}
ok = InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &size);
if (!ok) {
free(lpAttributeList);
goto error_close_stderr;
}
// Explicitly pass the HANDLEs that must be inherited
unsigned i = 0;
if (pin) {
handles[i++] = stdin_read_handle;
}
if (pout) {
handles[i++] = stdout_write_handle;
}
if (perr) {
handles[i++] = stderr_write_handle;
}
ok = UpdateProcThreadAttribute(lpAttributeList, 0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
handles, handle_count * sizeof(HANDLE),
NULL, NULL);
if (!ok) {
goto error_free_attribute_list;
}
si.lpAttributeList = lpAttributeList;
}
char *cmd = malloc(CMD_MAX_LEN);
if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) {
*handle = NULL;
goto error_close_stderr;
goto error_free_attribute_list;
}
wchar_t *wide = sc_str_to_wchars(cmd);
free(cmd);
if (!wide) {
LOGC("Could not allocate wide char string");
goto error_close_stderr;
goto error_free_attribute_list;
}
if (!CreateProcessW(NULL, wide, NULL, NULL, TRUE, 0, NULL, NULL, &si,
&pi)) {
free(wide);
*handle = NULL;
BOOL bInheritHandles = handle_count > 0;
DWORD dwCreationFlags = handle_count > 0 ? EXTENDED_STARTUPINFO_PRESENT : 0;
BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, bInheritHandles,
dwCreationFlags, NULL, NULL, &si.StartupInfo, &pi);
free(wide);
if (!ok) {
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
ret = SC_PROCESS_ERROR_MISSING_BINARY;
}
goto error_close_stderr;
goto error_free_attribute_list;
}
if (lpAttributeList) {
DeleteProcThreadAttributeList(lpAttributeList);
free(lpAttributeList);
}
// These handles are used by the child process, close them for this process
@ -117,11 +171,15 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle,
CloseHandle(stderr_write_handle);
}
free(wide);
*handle = pi.hProcess;
return SC_PROCESS_SUCCESS;
error_free_attribute_list:
if (lpAttributeList) {
DeleteProcThreadAttributeList(lpAttributeList);
free(lpAttributeList);
}
error_close_stderr:
if (perr) {
CloseHandle(*perr);
@ -168,7 +226,7 @@ sc_process_close(HANDLE handle) {
}
ssize_t
sc_read_pipe(HANDLE pipe, char *data, size_t len) {
sc_pipe_read(HANDLE pipe, char *data, size_t len) {
DWORD r;
if (!ReadFile(pipe, data, len, &r, NULL)) {
return -1;
@ -177,7 +235,7 @@ sc_read_pipe(HANDLE pipe, char *data, size_t len) {
}
void
sc_close_pipe(HANDLE pipe) {
sc_pipe_close(HANDLE pipe) {
if (!CloseHandle(pipe)) {
LOGW("Cannot close pipe");
}

View File

@ -14,12 +14,31 @@
* Component able to process and inject keys should implement this trait.
*/
struct sc_key_processor {
/**
* Set by the implementation to indicate that it must explicitly wait for
* the clipboard to be set on the device before injecting Ctrl+v to avoid
* race conditions. If it is set, the input_manager will pass a valid
* ack_to_wait to process_key() in case of clipboard synchronization
* resulting of the key event.
*/
bool async_paste;
const struct sc_key_processor_ops *ops;
};
struct sc_key_processor_ops {
/**
* Process the keyboard event
*
* The `sequence` number (if different from `SC_SEQUENCE_INVALID`) indicates
* the acknowledgement number to wait for before injecting this event.
* This allows to ensure that the device clipboard is set before injecting
* Ctrl+v on the device.
*/
void
(*process_key)(struct sc_key_processor *kp, const SDL_KeyboardEvent *event);
(*process_key)(struct sc_key_processor *kp, const SDL_KeyboardEvent *event,
uint64_t ack_to_wait);
void
(*process_text)(struct sc_key_processor *kp,

76
app/src/util/acksync.c Normal file
View File

@ -0,0 +1,76 @@
#include "acksync.h"
#include <assert.h>
#include "util/log.h"
bool
sc_acksync_init(struct sc_acksync *as) {
bool ok = sc_mutex_init(&as->mutex);
if (!ok) {
return false;
}
ok = sc_cond_init(&as->cond);
if (!ok) {
sc_mutex_destroy(&as->mutex);
return false;
}
as->stopped = false;
as->ack = SC_SEQUENCE_INVALID;
return true;
}
void
sc_acksync_destroy(struct sc_acksync *as) {
sc_cond_destroy(&as->cond);
sc_mutex_destroy(&as->mutex);
}
void
sc_acksync_ack(struct sc_acksync *as, uint64_t sequence) {
sc_mutex_lock(&as->mutex);
// Acknowledgements must be monotonic
assert(sequence >= as->ack);
as->ack = sequence;
sc_cond_signal(&as->cond);
sc_mutex_unlock(&as->mutex);
}
enum sc_acksync_wait_result
sc_acksync_wait(struct sc_acksync *as, uint64_t ack, sc_tick deadline) {
sc_mutex_lock(&as->mutex);
bool timed_out = false;
while (!as->stopped && as->ack < ack && !timed_out) {
timed_out = !sc_cond_timedwait(&as->cond, &as->mutex, deadline);
}
enum sc_acksync_wait_result ret;
if (as->stopped) {
ret = SC_ACKSYNC_WAIT_INTR;
} else if (as->ack >= ack) {
ret = SC_ACKSYNC_WAIT_OK;
} else {
assert(timed_out);
ret = SC_ACKSYNC_WAIT_TIMEOUT;
}
sc_mutex_unlock(&as->mutex);
return ret;
}
/**
* Interrupt any `sc_acksync_wait()`
*/
void
sc_acksync_interrupt(struct sc_acksync *as) {
sc_mutex_lock(&as->mutex);
as->stopped = true;
sc_cond_signal(&as->cond);
sc_mutex_unlock(&as->mutex);
}

66
app/src/util/acksync.h Normal file
View File

@ -0,0 +1,66 @@
#ifndef SC_ACK_SYNC_H
#define SC_ACK_SYNC_H
#include "common.h"
#include "thread.h"
#define SC_SEQUENCE_INVALID 0
/**
* Helper to wait for acknowledgments
*
* In practice, it is used to wait for device clipboard acknowledgement from the
* server before injecting Ctrl+v via AOA HID, in order to avoid pasting the
* content of the old device clipboard (if Ctrl+v was injected before the
* clipboard content was actually set).
*/
struct sc_acksync {
sc_mutex mutex;
sc_cond cond;
bool stopped;
// Last acked value, initially SC_SEQUENCE_INVALID
uint64_t ack;
};
enum sc_acksync_wait_result {
// Acknowledgment received
SC_ACKSYNC_WAIT_OK,
// Timeout expired
SC_ACKSYNC_WAIT_TIMEOUT,
// Interrupted from another thread by sc_acksync_interrupt()
SC_ACKSYNC_WAIT_INTR,
};
bool
sc_acksync_init(struct sc_acksync *as);
void
sc_acksync_destroy(struct sc_acksync *as);
/**
* Acknowledge `sequence`
*
* The `sequence` must be greater than (or equal to) any previous acknowledged
* sequence.
*/
void
sc_acksync_ack(struct sc_acksync *as, uint64_t sequence);
/**
* Wait for acknowledgment of sequence `ack` (or higher)
*/
enum sc_acksync_wait_result
sc_acksync_wait(struct sc_acksync *as, uint64_t ack, sc_tick deadline);
/**
* Interrupt any `sc_acksync_wait()`
*/
void
sc_acksync_interrupt(struct sc_acksync *as);
#endif

View File

@ -7,6 +7,7 @@
#include "log.h"
#ifdef __WINDOWS__
# include <ws2tcpip.h>
typedef int socklen_t;
typedef SOCKET sc_raw_socket;
#else
@ -225,3 +226,15 @@ net_close(sc_socket socket) {
return !close(raw_sock);
#endif
}
bool
net_parse_ipv4(const char *s, uint32_t *ipv4) {
struct in_addr addr;
if (!inet_pton(AF_INET, s, &addr)) {
LOGE("Invalid IPv4 address: %s", s);
return false;
}
*ipv4 = ntohl(addr.s_addr);
return true;
}

View File

@ -68,4 +68,10 @@ net_interrupt(sc_socket socket);
bool
net_close(sc_socket socket);
/**
* Parse `ip` "xxx.xxx.xxx.xxx" to an IPv4 host representation
*/
bool
net_parse_ipv4(const char *ip, uint32_t *ipv4);
#endif

View File

@ -2,7 +2,7 @@
bool
sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid,
const char *name) {
const char *name, bool close) {
if (!sc_intr_set_process(intr, pid)) {
// Already interrupted
return false;
@ -11,6 +11,40 @@ sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid,
// Always pass close=false, interrupting would be racy otherwise
bool ret = sc_process_check_success(pid, name, false);
sc_intr_set_process(intr, SC_PROCESS_NONE);
if (close) {
// Close separately
sc_process_close(pid);
}
return ret;
}
ssize_t
sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data,
size_t len) {
if (!sc_intr_set_process(intr, pid)) {
// Already interrupted
return false;
}
ssize_t ret = sc_pipe_read(pipe, data, len);
sc_intr_set_process(intr, SC_PROCESS_NONE);
return ret;
}
ssize_t
sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe,
char *data, size_t len) {
if (!sc_intr_set_process(intr, pid)) {
// Already interrupted
return false;
}
ssize_t ret = sc_pipe_read_all(pipe, data, len);
sc_intr_set_process(intr, SC_PROCESS_NONE);
return ret;
}

View File

@ -8,6 +8,14 @@
bool
sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid,
const char *name);
const char *name, bool close);
ssize_t
sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data,
size_t len);
ssize_t
sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe,
char *data, size_t len);
#endif

View File

@ -291,3 +291,11 @@ error:
free(buf.s);
return NULL;
}
size_t
sc_str_truncate(char *data, size_t len, const char *endchars) {
data[len - 1] = '\0';
size_t idx = strcspn(data, endchars);
data[idx] = '\0';
return idx;
}

View File

@ -103,4 +103,15 @@ sc_str_from_wchars(const wchar_t *s);
char *
sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent);
/**
* Truncate the data after any of the characters from `endchars`
*
* An '\0' is always written at the end of the data, even if no newline
* character is encountered.
*
* Return the size of the resulting line.
*/
size_t
sc_str_truncate(char *data, size_t len, const char *endchars);
#endif

View File

@ -78,7 +78,7 @@ static void test_serialize_inject_touch_event(void) {
.type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
.inject_touch_event = {
.action = AMOTION_EVENT_ACTION_DOWN,
.pointer_id = 0x1234567887654321L,
.pointer_id = UINT64_C(0x1234567887654321),
.position = {
.point = {
.x = 100,
@ -226,6 +226,7 @@ static void test_serialize_set_clipboard(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_SET_CLIPBOARD,
.set_clipboard = {
.sequence = UINT64_C(0x0102030405060708),
.paste = true,
.text = "hello, world!",
},
@ -233,10 +234,11 @@ static void test_serialize_set_clipboard(void) {
unsigned char buf[CONTROL_MSG_MAX_SIZE];
size_t size = control_msg_serialize(&msg, buf);
assert(size == 19);
assert(size == 27);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_SET_CLIPBOARD,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence
1, // paste
0x00, 0x00, 0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text

View File

@ -47,11 +47,26 @@ static void test_deserialize_clipboard_big(void) {
device_msg_destroy(&msg);
}
static void test_deserialize_ack_set_clipboard(void) {
const unsigned char input[] = {
DEVICE_MSG_TYPE_ACK_CLIPBOARD,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence
};
struct device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg);
assert(r == 9);
assert(msg.type == DEVICE_MSG_TYPE_ACK_CLIPBOARD);
assert(msg.ack_clipboard.sequence == UINT64_C(0x0102030405060708));
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_deserialize_clipboard();
test_deserialize_clipboard_big();
test_deserialize_ack_set_clipboard();
return 0;
}

View File

@ -337,6 +337,32 @@ static void test_wrap_lines(void) {
free(formatted);
}
static void test_truncate(void) {
char s[] = "hello\nworld\n!";
size_t len = sc_str_truncate(s, sizeof(s), "\n");
assert(len == 5);
assert(!strcmp("hello", s));
char s2[] = "hello\r\nworkd\r\n!";
len = sc_str_truncate(s2, sizeof(s2), "\n\r");
assert(len == 5);
assert(!strcmp("hello", s));
char s3[] = "hello world\n!";
len = sc_str_truncate(s3, sizeof(s3), " \n\r");
assert(len == 5);
assert(!strcmp("hello", s3));
char s4[] = "hello ";
len = sc_str_truncate(s4, sizeof(s4), " \n\r");
assert(len == 5);
assert(!strcmp("hello", s4));
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
@ -356,5 +382,6 @@ int main(int argc, char *argv[]) {
test_parse_integer_with_suffix();
test_strlist_contains();
test_wrap_lines();
test_truncate();
return 0;
}

View File

@ -1,6 +1,5 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Parcel;
@ -166,14 +165,21 @@ public final class CleanUp {
if (config.disableShowTouches || config.restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager();
try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) {
if (config.disableShowTouches) {
Ln.i("Disabling \"show touches\"");
settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0");
Settings settings = new Settings(serviceManager);
if (config.disableShowTouches) {
Ln.i("Disabling \"show touches\"");
try {
settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
} catch (SettingsException e) {
Ln.e("Could not restore \"show_touches\"", e);
}
if (config.restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
}
if (config.restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
try {
settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
} catch (SettingsException e) {
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
}
}
}

View File

@ -0,0 +1,33 @@
package com.genymobile.scrcpy;
import java.io.IOException;
import java.util.Arrays;
import java.util.Scanner;
public final class Command {
private Command() {
// not instantiable
}
public static void exec(String... cmd) throws IOException, InterruptedException {
Process process = Runtime.getRuntime().exec(cmd);
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
}
}
public static String execReadLine(String... cmd) throws IOException, InterruptedException {
String result = null;
Process process = Runtime.getRuntime().exec(cmd);
Scanner scanner = new Scanner(process.getInputStream());
if (scanner.hasNextLine()) {
result = scanner.nextLine();
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
}
return result;
}
}

View File

@ -18,6 +18,8 @@ public final class ControlMessage {
public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
public static final int TYPE_ROTATE_DEVICE = 11;
public static final long SEQUENCE_INVALID = 0;
private int type;
private String text;
private int metaState; // KeyEvent.META_*
@ -31,6 +33,7 @@ public final class ControlMessage {
private int vScroll;
private boolean paste;
private int repeat;
private long sequence;
private ControlMessage() {
}
@ -79,9 +82,10 @@ public final class ControlMessage {
return msg;
}
public static ControlMessage createSetClipboard(String text, boolean paste) {
public static ControlMessage createSetClipboard(long sequence, String text, boolean paste) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD;
msg.sequence = sequence;
msg.text = text;
msg.paste = paste;
return msg;
@ -154,4 +158,8 @@ public final class ControlMessage {
public int getRepeat() {
return repeat;
}
public long getSequence() {
return sequence;
}
}

View File

@ -13,11 +13,11 @@ public class ControlMessageReader {
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9;
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 6; // type: 1 byte; paste flag: 1 byte; length: 4 bytes
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14; // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes
public static final int INJECT_TEXT_MAX_LENGTH = 300;
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
@ -166,12 +166,13 @@ public class ControlMessageReader {
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
return null;
}
long sequence = buffer.getLong();
boolean paste = buffer.get() != 0;
String text = parseString();
if (text == null) {
return null;
}
return ControlMessage.createSetClipboard(text, paste);
return ControlMessage.createSetClipboard(sequence, text, paste);
}
private ControlMessage parseSetScreenPowerMode() {

View File

@ -120,7 +120,12 @@ public class Controller {
}
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
long sequence = msg.getSequence();
setClipboard(msg.getText(), msg.getPaste());
if (sequence != ControlMessage.SEQUENCE_INVALID) {
// Acknowledgement requested
sender.pushAckClipboard(sequence);
}
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
if (device.supportsInputEvents()) {

View File

@ -1,7 +1,6 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
@ -29,6 +28,7 @@ public final class Device {
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
private static final Settings SETTINGS = new Settings(SERVICE_MANAGER);
public interface RotationListener {
void onRotationChanged(int rotation);
@ -82,8 +82,8 @@ public final class Device {
}
}, displayId);
if (options.getControl()) {
// If control is enabled, synchronize Android clipboard to the computer automatically
if (options.getControl() && options.getClipboardAutosync()) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
if (clipboardManager != null) {
clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
@ -296,7 +296,7 @@ public final class Device {
}
}
public static ContentProvider createSettingsProvider() {
return SERVICE_MANAGER.getActivityManager().createSettingsProvider();
public static Settings getSettings() {
return SETTINGS;
}
}

View File

@ -3,9 +3,13 @@ package com.genymobile.scrcpy;
public final class DeviceMessage {
public static final int TYPE_CLIPBOARD = 0;
public static final int TYPE_ACK_CLIPBOARD = 1;
public static final long SEQUENCE_INVALID = ControlMessage.SEQUENCE_INVALID;
private int type;
private String text;
private long sequence;
private DeviceMessage() {
}
@ -17,6 +21,13 @@ public final class DeviceMessage {
return event;
}
public static DeviceMessage createAckClipboard(long sequence) {
DeviceMessage event = new DeviceMessage();
event.type = TYPE_ACK_CLIPBOARD;
event.sequence = sequence;
return event;
}
public int getType() {
return type;
}
@ -24,4 +35,8 @@ public final class DeviceMessage {
public String getText() {
return text;
}
public long getSequence() {
return sequence;
}
}

View File

@ -8,6 +8,8 @@ public final class DeviceMessageSender {
private String clipboardText;
private long ack;
public DeviceMessageSender(DesktopConnection connection) {
this.connection = connection;
}
@ -17,18 +19,34 @@ public final class DeviceMessageSender {
notify();
}
public synchronized void pushAckClipboard(long sequence) {
ack = sequence;
notify();
}
public void loop() throws IOException, InterruptedException {
while (true) {
String text;
long sequence;
synchronized (this) {
while (clipboardText == null) {
while (ack == DeviceMessage.SEQUENCE_INVALID && clipboardText == null) {
wait();
}
text = clipboardText;
clipboardText = null;
sequence = ack;
ack = DeviceMessage.SEQUENCE_INVALID;
}
if (sequence != DeviceMessage.SEQUENCE_INVALID) {
DeviceMessage event = DeviceMessage.createAckClipboard(sequence);
connection.sendDeviceMessage(event);
}
if (text != null) {
DeviceMessage event = DeviceMessage.createClipboard(text);
connection.sendDeviceMessage(event);
}
DeviceMessage event = DeviceMessage.createClipboard(text);
connection.sendDeviceMessage(event);
}
}
}

View File

@ -15,7 +15,7 @@ public class DeviceMessageWriter {
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
buffer.clear();
buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD);
buffer.put((byte) msg.getType());
switch (msg.getType()) {
case DeviceMessage.TYPE_CLIPBOARD:
String text = msg.getText();
@ -25,6 +25,10 @@ public class DeviceMessageWriter {
buffer.put(raw, 0, len);
output.write(rawBuffer, 0, buffer.position());
break;
case DeviceMessage.TYPE_ACK_CLIPBOARD:
buffer.putLong(msg.getSequence());
output.write(rawBuffer, 0, buffer.position());
break;
default:
Ln.w("Unknown device message: " + msg.getType());
break;

View File

@ -57,13 +57,20 @@ public final class Ln {
}
}
public static void w(String message) {
public static void w(String message, Throwable throwable) {
if (isEnabled(Level.WARN)) {
Log.w(TAG, message);
Log.w(TAG, message, throwable);
System.out.println(PREFIX + "WARN: " + message);
if (throwable != null) {
throwable.printStackTrace();
}
}
}
public static void w(String message) {
w(message, null);
}
public static void e(String message, Throwable throwable) {
if (isEnabled(Level.ERROR)) {
Log.e(TAG, message, throwable);

View File

@ -18,6 +18,7 @@ public class Options {
private String codecOptions;
private String encoderName;
private boolean powerOffScreenOnClose;
private boolean clipboardAutosync;
public Ln.Level getLogLevel() {
return logLevel;
@ -138,4 +139,12 @@ public class Options {
public boolean getPowerOffScreenOnClose() {
return this.powerOffScreenOnClose;
}
public boolean getClipboardAutosync() {
return clipboardAutosync;
}
public void setClipboardAutosync(boolean clipboardAutosync) {
this.clipboardAutosync = clipboardAutosync;
}
}

View File

@ -1,7 +1,5 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
@ -19,24 +17,25 @@ public final class Server {
// not instantiable
}
private static void scrcpy(Options options) throws IOException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options);
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
private static void initAndCleanUp(Options options) {
boolean mustDisableShowTouchesOnCleanUp = false;
int restoreStayOn = -1;
if (options.getShowTouches() || options.getStayAwake()) {
try (ContentProvider settings = Device.createSettingsProvider()) {
if (options.getShowTouches()) {
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1");
Settings settings = Device.getSettings();
if (options.getShowTouches()) {
try {
String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
}
}
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
try {
String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
try {
restoreStayOn = Integer.parseInt(oldValue);
if (restoreStayOn == stayOn) {
@ -46,11 +45,25 @@ public final class Server {
} catch (NumberFormatException e) {
restoreStayOn = 0;
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
}
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
try {
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
} catch (IOException e) {
Ln.e("Could not configure cleanup", e);
}
}
private static void scrcpy(Options options) throws IOException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options);
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
Thread initThread = startInitThread(options);
boolean tunnelForward = options.isTunnelForward();
@ -82,6 +95,7 @@ public final class Server {
// this is expected on close
Ln.d("Screen streaming stopped");
} finally {
initThread.interrupt();
if (controllerThread != null) {
controllerThread.interrupt();
}
@ -92,6 +106,17 @@ public final class Server {
}
}
private static Thread startInitThread(final Options 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
@ -135,7 +160,7 @@ public final class Server {
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
final int expectedParameters = 16;
final int expectedParameters = 17;
if (args.length != expectedParameters) {
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
}
@ -188,6 +213,9 @@ public final class Server {
boolean powerOffScreenOnClose = Boolean.parseBoolean(args[15]);
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
boolean clipboardAutosync = Boolean.parseBoolean(args[16]);
options.setClipboardAutosync(clipboardAutosync);
return options;
}

View File

@ -0,0 +1,84 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build;
import java.io.IOException;
public class Settings {
public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM;
public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE;
public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL;
private final ServiceManager serviceManager;
public Settings(ServiceManager serviceManager) {
this.serviceManager = serviceManager;
}
private static void execSettingsPut(String table, String key, String value) throws SettingsException {
try {
Command.exec("settings", "put", table, key, value);
} catch (IOException | InterruptedException e) {
throw new SettingsException("put", table, key, value, e);
}
}
private static String execSettingsGet(String table, String key) throws SettingsException {
try {
return Command.execReadLine("settings", "get", table, key);
} catch (IOException | InterruptedException e) {
throw new SettingsException("get", table, key, null, e);
}
}
public String getValue(String table, String key) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
return provider.getValue(table, key);
} catch (SettingsException e) {
Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e);
}
}
return execSettingsGet(table, key);
}
public void putValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
provider.putValue(table, key, value);
} catch (SettingsException e) {
Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e);
}
}
execSettingsPut(table, key, value);
}
public String getAndPutValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
String oldValue = provider.getValue(table, key);
if (!value.equals(oldValue)) {
provider.putValue(table, key, value);
}
return oldValue;
} catch (SettingsException e) {
Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e);
}
}
String oldValue = getValue(table, key);
if (!value.equals(oldValue)) {
putValue(table, key, value);
}
return oldValue;
}
}

View File

@ -0,0 +1,11 @@
package com.genymobile.scrcpy;
public class SettingsException extends Exception {
private static String createMessage(String method, String table, String key, String value) {
return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : "");
}
public SettingsException(String method, String table, String key, String value, Throwable cause) {
super(createMessage(method, table, key, value), cause);
}
}

View File

@ -1,6 +1,7 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.SettingsException;
import android.annotation.SuppressLint;
import android.os.Bundle;
@ -87,7 +88,8 @@ public class ContentProvider implements Closeable {
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 {
try {
Method method = getCallMethod();
Object[] args;
@ -108,7 +110,7 @@ public class ContentProvider implements Closeable {
return (Bundle) method.invoke(provider, args);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
Ln.e("Could not invoke method", e);
return null;
throw e;
}
}
@ -142,30 +144,31 @@ public class ContentProvider implements Closeable {
}
}
public String getValue(String table, String key) {
public String getValue(String table, String key) throws SettingsException {
String method = getGetMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
Bundle bundle = call(method, key, arg);
if (bundle == null) {
return null;
try {
Bundle bundle = call(method, key, arg);
if (bundle == null) {
return null;
}
return bundle.getString("value");
} catch (Exception e) {
throw new SettingsException(table, "get", key, null, e);
}
return bundle.getString("value");
}
public void putValue(String table, String key, String value) {
public void putValue(String table, String key, String value) throws SettingsException {
String method = getPutMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
arg.putString(NAME_VALUE_TABLE_VALUE, value);
call(method, key, arg);
}
public String getAndPutValue(String table, String key, String value) {
String oldValue = getValue(table, key);
if (!value.equals(oldValue)) {
putValue(table, key, value);
try {
call(method, key, arg);
} catch (Exception e) {
throw new SettingsException(table, "put", key, value, e);
}
return oldValue;
}
}

View File

@ -235,6 +235,7 @@ public class ControlMessageReaderTest {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
dos.writeLong(0x0102030405060708L); // sequence
dos.writeByte(1); // paste
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeInt(text.length);
@ -246,6 +247,7 @@ public class ControlMessageReaderTest {
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals(0x0102030405060708L, event.getSequence());
Assert.assertEquals("testé", event.getText());
Assert.assertTrue(event.getPaste());
}
@ -259,6 +261,7 @@ public class ControlMessageReaderTest {
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH];
dos.writeLong(0x0807060504030201L); // sequence
dos.writeByte(1); // paste
Arrays.fill(rawText, (byte) 'a');
String text = new String(rawText, 0, rawText.length);
@ -272,6 +275,7 @@ public class ControlMessageReaderTest {
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals(0x0807060504030201L, event.getSequence());
Assert.assertEquals(text, event.getText());
Assert.assertTrue(event.getPaste());
}

View File

@ -32,4 +32,24 @@ public class DeviceMessageWriterTest {
Assert.assertArrayEquals(expected, actual);
}
@Test
public void testSerializeAckSetClipboard() throws IOException {
DeviceMessageWriter writer = new DeviceMessageWriter();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD);
dos.writeLong(0x0102030405060708L);
byte[] expected = bos.toByteArray();
DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L);
bos = new ByteArrayOutputStream();
writer.writeTo(msg, bos);
byte[] actual = bos.toByteArray();
Assert.assertArrayEquals(expected, actual);
}
}