Compare commits

...

90 Commits

Author SHA1 Message Date
Romain Vimont 6b269b2352 Add --raw-key-events
This option allows to inject all input keys as key events, and ignore
text events.

Fixes #2816 <https://github.com/Genymobile/scrcpy/issues/2816>
2021-11-26 22:15:44 +01:00
Romain Vimont d733b9d578 Use enum for key injection mode
There was only two key injection modes:
 - the default one
 - the mode with --prefer-text enabled

To prepare the addition of another mode (--raw-key-events), use an enum
instead of a bool.
2021-11-26 22:05:28 +01:00
Romain Vimont ec707b95f8 Use static maps to convert input events
This improves readability (hopefully).
2021-11-26 21:58:56 +01:00
Romain Vimont 94b0f2d2c1 Fix code style in keyboard_inject 2021-11-26 21:57:06 +01:00
Romain Vimont f1c53c441d Improve HID keyboard documentation
Explain how to configure the keyboard layout.
2021-11-26 21:56:28 +01:00
Romain Vimont 8429e43171 Document --tcpip in README 2021-11-26 09:41:11 +01:00
Romain Vimont 785a0e8e8a Add --tcpip feature
Expose an option to automatically configure and reconnect the device
over TCP/IP, to simplify wireless connection without using adb
explicitly.

There are two variants:
 - If a destination address is provided, then scrcpy connects to this
   address before starting. The device must listen on the given TCP port
   (default is 5555).
 - If no destination address is provided, then scrcpy attempts to find
   the IP address of the current device (typically connected over USB),
   enables TCP/IP mode, then connects to this address before starting.
2021-11-26 09:41:11 +01:00
Romain Vimont d8987b96ef Extract interruptible sleep for server
This improves readability, and makes the function reusable.
2021-11-26 09:41:11 +01:00
Romain Vimont fa9fa93266 Add function to get the current adbd port
This will allow to know if the TCP/IP mode is enabled on the device, and
which listening port is used.
2021-11-26 09:41:11 +01:00
Romain Vimont 3bb7f9b54e Add function to switch device to TCP/IP mode
Expose a function to execute "adb tcpip <port>".
2021-11-26 09:41:11 +01:00
Romain Vimont 93eb667a4e Add function to find the device IP address
Parse the result of "adb shell ip route" to find the device IP address.
2021-11-26 09:41:11 +01:00
Romain Vimont 799595b5cb Add util function to locate a column in a string
This will help to parse the result of "adb shell ip route" to find the
device IP address.
2021-11-26 09:41:11 +01:00
Romain Vimont 3c8cce5d77 Workaround "adb connect" error detection
"adb connect" always returns successfully (with exit code 0), even in
case of failure.

As a workaround, capture its output and check if it starts with
"connected".
2021-11-26 09:41:11 +01:00
Romain Vimont d0695682d7 Add adb connect and disconnect 2021-11-26 09:41:11 +01:00
Romain Vimont a59590a8c5 Add adb flag to disable execution error logs
In addition to disable stdout and stderr of the child process, add a
flag to disable the error log printed by scrcpy if the command failed.

This will we useful for commands which are expected to fail in some
cases (like "adb disconnect" if the device is not connected).
2021-11-26 09:41:11 +01:00
Romain Vimont a26a408d59 Use flags for adb commands
Explicitly indicate, for each adb call, if stdout and stderr must be
inherited.
2021-11-26 09:41:11 +01:00
Romain Vimont 01d6d8c41f Expose flags for process execution
Let the caller decide if stdout and stderr must be inherited on process
creation, i.e. if stdout and stderr of the child process should be
printed in the scrcpy console.

This allows to get output and errors for specific adb commands depending
on the context.
2021-11-26 09:41:11 +01:00
Romain Vimont b9b8b6aab8 Simplify Windows process inheritance configuration
Merge if-blocks together.
2021-11-26 09:41:11 +01:00
Romain Vimont 3e54773c48 Remove intermediate static functions from adb.c
They can easily be inlined into the public functions.
2021-11-26 09:41:11 +01:00
Romain Vimont b90c89766b Set CLOEXEC flag on sockets
If SOCK_CLOEXEC exists, then set the flag on socket creation.

Otherwise, use fcntl() (or SetHandleInformation() on Windows) to set the
flag afterwards.

This avoids the sockets to be inherited in child processes.

Refs #2783 <https://github.com/Genymobile/scrcpy/pull/2783>
2021-11-26 09:40:39 +01:00
Romain Vimont 904f0ae61e Check process success locally for adb commands
Remove sc_process_check_success() from the process API, it is too
specific.
2021-11-25 22:22:34 +01:00
Romain Vimont 680d2cc940 Extract command argv building
This simplifies adb_execute_p().
2021-11-25 22:22:34 +01:00
Romain Vimont 8ed3328055 Use unsigned for connection attempts count
There is no reason to use an explicit uint32_t.
2021-11-25 22:22:34 +01:00
Romain Vimont d31725f077 Reorder cli sanity checks
Check unexpected additional arguments before other sanity checks.
2021-11-25 22:22:34 +01:00
Romain Vimont b0eb1a55d6 Fix adb get-serialno error handling
If pipe read fails, return.
2021-11-25 22:22:34 +01:00
Romain Vimont 3653fb6b15 Add OutOfMemory log helper
Add a special LOG_OOM() function to log all OutOfMemory errors (i.e.
allocations returning NULL).
2021-11-24 22:06:49 +01:00
Romain Vimont 92a458e846 Remove unreachable return statements 2021-11-24 21:48:57 +01:00
Romain Vimont 73e0311d14 Add missing return on file_handler failure
Mistake introduced by 84334cf7db.
2021-11-24 21:46:02 +01:00
Romain Vimont 007f616302 Add missing includes
Include these headers explicitly instead of relying on transitivity.
2021-11-24 21:44:29 +01:00
Romain Vimont 4c47598865 Make lockVideoOrientation option name uniform
On the server, the option was named lockedVideoOrientation.
2021-11-24 21:33:58 +01:00
Romain Vimont 7b29f5fd2d Do not pass default values to the server
By default, only pass the version and the bit rate (the default bitrate
is a client compilation option).
2021-11-24 21:31:12 +01:00
Romain Vimont 04e5537f8c Pass server parameters as key=value pairs
The options values to configure the server were identified by their
command-line argument index. Now that there are a lot of arguments, many
of them being booleans, it became unreadable and error-prone.

Identify the arguments by a key string instead, and make them optional.

This will also simplify running the server manually for debugging.
2021-11-24 21:23:20 +01:00
Romain Vimont 2eb881c5f1 Allocate and format server command args
This simplifies formatting.
2021-11-24 21:10:18 +01:00
Romain Vimont 2c3099e2de Parse codec options early
For consistency with other options, parse the codec options on the
server before storing them in the Options instance.
2021-11-24 21:06:02 +01:00
Romain Vimont 5434ea543c Remove local "serial" variable
In execute_server(), the serial is used only once. Moreover, it can be
retrieved from the `params` argument directly.
2021-11-24 21:01:25 +01:00
Romain Vimont d5f6697f3a Add (v)asprintf compatibility functions
In case they are not available on the platform.
2021-11-24 19:55:00 +01:00
Romain Vimont d6c0054545 Move check_functions in meson script
Move the variable to the place it is actually used.
2021-11-24 19:38:33 +01:00
Romain Vimont 6f487a2892 Add missing includes in compat implementation
These includes are necessary for the strdup() compat implementation.
2021-11-24 19:37:33 +01:00
Romain Vimont dc0ac01e00 Define common feature test macros for all systems
_POSIX_C_SOURCE, _XOPEN_SOURCE and _GNU_SOURCE are also used on Windows.

Fix regression introduced by ba547e3895.
2021-11-24 19:36:33 +01:00
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
86 changed files with 2487 additions and 675 deletions

117
README.md
View File

@ -5,7 +5,7 @@
[Read in another language](#translations)
This application provides display and control of Android devices connected via
USB (or [over TCP/IP](#wireless)). It does not require any _root_ access.
USB (or [over TCP/IP](#tcpip-wireless)). It does not require any _root_ access.
It works on _GNU/Linux_, _Windows_ and _macOS_.
![screenshot](assets/screenshot-debian-600.jpg)
@ -356,10 +356,38 @@ scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink
### Connection
#### Wireless
#### TCP/IP (wireless)
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
device over TCP/IP:
device over TCP/IP.
##### Automatic
An option `--tcpip` allows to configure the connection automatically. There are
two variants.
If the device (accessible at 192.168.1.1 in this example) already listens on a
port (typically 5555) for incoming adb connections, then run:
```bash
scrcpy --tcpip=192.168.1.1 # default port is 5555
scrcpy --tcpip=192.168.1.1:5555
```
If the device TCP/IP mode is disabled (or if you don't know the IP address),
connect the device over USB, then run:
```bash
scrcpy --tcpip # without arguments
```
It will automatically find the device IP address, enable TCP/IP mode, then
connects to the device before starting.
##### Manual
Alternatively, it is possible to enable the TCP/IP connection manually using
`adb`:
1. Connect the device to the same Wi-Fi as your computer.
2. Get your device IP address, in Settings → About phone → Status, or by
@ -412,21 +440,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 +507,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 +758,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_.
@ -725,6 +803,15 @@ of the host key mapping. Therefore, if your keyboard layout does not match, it
must be configured on the Android device, in Settings → System → Languages and
input → [Physical keyboard].
This settings page can be started directly:
```bash
adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS
```
However, the option is only available when the HID keyboard is enabled (or when
a physical keyboard is connected).
[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915
@ -746,7 +833,13 @@ scrcpy --prefer-text
(but this will break keyboard behavior in games)
This option has no effect on HID keyboard (all key events are sent as
On the contrary, you could force to always inject raw key events:
```bash
scrcpy --raw-key-events
```
These options have no effect on HID keyboard (all key events are sent as
scancodes in this mode).
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input

View File

@ -25,7 +25,9 @@ src = [
'src/server.c',
'src/stream.c',
'src/video_buffer.c',
'src/util/acksync.c',
'src/util/file.c',
'src/util/intmap.c',
'src/util/intr.c',
'src/util/log.c',
'src/util/net.c',
@ -39,16 +41,27 @@ src = [
'src/util/tick.c',
]
conf = configuration_data()
conf.set('_POSIX_C_SOURCE', '200809L')
conf.set('_XOPEN_SOURCE', '700')
conf.set('_GNU_SOURCE', true)
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',
]
if host_machine.system() == 'darwin'
conf.set('_DARWIN_C_SOURCE', true)
endif
endif
v4l2_support = host_machine.system() == 'linux'
@ -64,10 +77,6 @@ if aoa_hid_support
]
endif
check_functions = [
'strdup'
]
cc = meson.get_compiler('c')
if not get_option('crossbuild_windows')
@ -128,7 +137,11 @@ if host_machine.system() == 'windows'
dependencies += cc.find_library('ws2_32')
endif
conf = configuration_data()
check_functions = [
'strdup',
'asprintf',
'vasprintf',
]
foreach f : check_functions
if cc.has_function(f)
@ -137,6 +150,9 @@ foreach f : check_functions
endif
endforeach
conf.set('HAVE_SOCK_CLOEXEC', host_machine.system() != 'windows' and
cc.has_header_symbol('sys/socket.h', 'SOCK_CLOEXEC'))
# the version, updated on release
conf.set_quoted('SCRCPY_VERSION', meson.project_version())
@ -199,6 +215,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

@ -90,6 +90,12 @@ This provides a better experience for IME users, and allows to generate non-ASCI
It may only work over USB, and is currently only supported on Linux.
The keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly:
adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS
However, the option is only available when the HID keyboard is enabled (or a physical keyboard is connected).
.TP
.B \-\-legacy\-paste
Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v).
@ -114,6 +120,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).
@ -153,6 +165,10 @@ Set the target directory for pushing files to the device by drag & drop. It is p
Default is "/sdcard/Download/".
.TP
.B \-\-raw\-key\-events
Inject key events for all input keys, and ignore text events.
.TP
.BI "\-r, \-\-record " file
Record screen to
@ -193,6 +209,14 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr
Default is "lalt,lsuper" (left-Alt or left-Super).
.TP
.BI "\-\-tcpip[=ip[:port]]
Configure and reconnect the device over TCP/IP.
If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555).
If no destination address is provided, then scrcpy attempts to find the IP address of the current device (typically connected over USB), enables TCP/IP mode, then connects to this address before starting.
.TP
.B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately.
@ -203,6 +227,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;
@ -85,7 +86,8 @@ show_adb_err_msg(enum sc_process_result err, const char *const argv[]) {
#define MAX_COMMAND_STRING_LEN 1024
char *buf = malloc(MAX_COMMAND_STRING_LEN);
if (!buf) {
LOGE("Failed to execute (could not allocate error message)");
LOG_OOM();
LOGE("Failed to execute");
return;
}
@ -109,18 +111,61 @@ show_adb_err_msg(enum sc_process_result err, const char *const argv[]) {
free(buf);
}
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) {
int i;
sc_pid pid;
static bool
process_check_success_internal(sc_pid pid, const char *name, bool close,
unsigned flags) {
bool log_errors = !(flags & SC_ADB_NO_LOGERR);
if (pid == SC_PROCESS_NONE) {
if (log_errors) {
LOGE("Could not execute \"%s\"", name);
}
return false;
}
sc_exit_code exit_code = sc_process_wait(pid, close);
if (exit_code) {
if (log_errors) {
if (exit_code != SC_EXIT_CODE_NONE) {
LOGE("\"%s\" returned with value %" SC_PRIexitcode, name,
exit_code);
} else {
LOGE("\"%s\" exited unexpectedly", name);
}
}
return false;
}
return true;
}
static bool
process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name,
unsigned flags) {
if (!sc_intr_set_process(intr, pid)) {
// Already interrupted
return false;
}
// Always pass close=false, interrupting would be racy otherwise
bool ret = process_check_success_internal(pid, name, false, flags);
sc_intr_set_process(intr, SC_PROCESS_NONE);
// Close separately
sc_process_close(pid);
return ret;
}
static const char **
adb_create_argv(const char *serial, const char *const adb_cmd[], size_t len) {
const char **argv = malloc((len + 4) * sizeof(*argv));
if (!argv) {
return SC_PROCESS_NONE;
LOG_OOM();
return NULL;
}
argv[0] = get_adb_command();
int i;
if (serial) {
argv[1] = "-s";
argv[2] = serial;
@ -131,9 +176,31 @@ adb_execute_p(const char *serial, const char *const adb_cmd[],
memcpy(&argv[i], adb_cmd, len * sizeof(const char *));
argv[len + i] = NULL;
return argv;
}
static sc_pid
adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len,
unsigned flags, sc_pipe *pout) {
const char **argv = adb_create_argv(serial, adb_cmd, len);
if (!argv) {
return SC_PROCESS_NONE;
}
unsigned process_flags = 0;
if (flags & SC_ADB_NO_STDOUT) {
process_flags |= SC_PROCESS_NO_STDOUT;
}
if (flags & SC_ADB_NO_STDERR) {
process_flags |= SC_PROCESS_NO_STDERR;
}
sc_pid pid;
enum sc_process_result r =
sc_process_execute_p(argv, &pid, pin, pout, perr);
sc_process_execute_p(argv, &pid, process_flags, NULL, pout, NULL);
if (r != SC_PROCESS_SUCCESS) {
// If the execution itself failed (not the command exit code), log the
// error anyway
show_adb_err_msg(r, argv);
pid = SC_PROCESS_NONE;
}
@ -143,50 +210,63 @@ 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);
adb_execute(const char *serial, const char *const adb_cmd[], size_t len,
unsigned flags) {
return adb_execute_p(serial, adb_cmd, len, flags, NULL);
}
sc_pid
adb_forward(const char *serial, uint16_t local_port,
const char *device_socket_name) {
bool
adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port,
const char *device_socket_name, unsigned flags) {
char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME
sprintf(local, "tcp:%" PRIu16, local_port);
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name);
const char *const adb_cmd[] = {"forward", local, remote};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags);
return process_check_success_intr(intr, pid, "adb forward", flags);
}
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, unsigned flags) {
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 pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags);
return process_check_success_intr(intr, pid, "adb forward --remove", flags);
}
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,
unsigned flags) {
char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME
sprintf(local, "tcp:%" PRIu16, local_port);
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name);
const char *const adb_cmd[] = {"reverse", remote, local};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags);
return process_check_success_intr(intr, pid, "adb reverse", flags);
}
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, unsigned flags) {
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 pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags);
return process_check_success_intr(intr, pid, "adb reverse --remove", flags);
}
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, unsigned flags) {
#ifdef __WINDOWS__
// Windows will parse the string, so the paths must be quoted
// (see sys/win/command.c)
@ -202,18 +282,19 @@ adb_push(const char *serial, const char *local, const char *remote) {
#endif
const char *const adb_cmd[] = {"push", local, remote};
sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags);
#ifdef __WINDOWS__
free((void *) remote);
free((void *) local);
#endif
return pid;
return process_check_success_intr(intr, pid, "adb push", flags);
}
sc_pid
adb_install(const char *serial, const char *local) {
bool
adb_install(struct sc_intr *intr, const char *serial, const char *local,
unsigned flags) {
#ifdef __WINDOWS__
// Windows will parse the string, so the local name must be quoted
// (see sys/win/command.c)
@ -224,54 +305,167 @@ adb_install(const char *serial, const char *local) {
#endif
const char *const adb_cmd[] = {"install", "-r", local};
sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags);
#ifdef __WINDOWS__
free((void *) local);
#endif
return pid;
return process_check_success_intr(intr, pid, "adb install", flags);
}
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);
bool
adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port,
unsigned flags) {
char port_string[5 + 1];
sprintf(port_string, "%" PRIu16, port);
const char *const adb_cmd[] = {"tcpip", port_string};
ssize_t r = sc_pipe_read_all(pout, buf, buf_len);
sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags);
return process_check_success_intr(intr, pid, "adb tcpip", flags);
}
bool
adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) {
const char *const adb_cmd[] = {"connect", ip_port};
sc_pipe pout;
sc_pid pid = adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout);
if (pid == SC_PROCESS_NONE) {
LOGE("Could not execute \"adb connect\"");
return false;
}
// "adb connect" always returns successfully (with exit code 0), even in
// case of failure. As a workaround, check if its output starts with
// "connected".
char buf[128];
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf));
sc_pipe_close(pout);
if (!sc_process_check_success(pid, name, true)) {
return -1;
bool ok = process_check_success_intr(intr, pid, "adb connect", flags);
if (!ok) {
return false;
}
return r;
if (r == -1) {
return false;
}
ok = !strncmp("connected", buf, sizeof("connected") - 1);
if (!ok && !(flags & SC_ADB_NO_STDERR)) {
// "adb connect" also prints errors to stdout. Since we capture it,
// re-print the error to stderr.
sc_str_truncate(buf, r, "\r\n");
fprintf(stderr, "%s\n", buf);
}
return ok;
}
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_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) {
const char *const adb_cmd[] = {"disconnect", ip_port};
size_t len = ip_port ? ARRAY_LEN(adb_cmd)
: ARRAY_LEN(adb_cmd) - 1;
sc_pid pid = adb_execute(NULL, adb_cmd, len, flags);
return process_check_success_intr(intr, pid, "adb disconnect", flags);
}
char *
adb_get_serialno(void) {
char buf[128];
adb_getprop(struct sc_intr *intr, const char *serial, const char *prop,
unsigned flags) {
const char *const adb_cmd[] = {"shell", "getprop", prop};
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) {
sc_pipe pout;
sc_pid pid =
adb_execute_p(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout);
if (pid == SC_PROCESS_NONE) {
LOGE("Could not execute \"adb getprop\"");
return NULL;
}
truncate_first_line(buf, r);
char buf[16];
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf));
sc_pipe_close(pout);
bool ok = process_check_success_intr(intr, pid, "adb getprop", flags);
if (!ok) {
return NULL;
}
if (r == -1) {
return NULL;
}
sc_str_truncate(buf, r, " \r\n");
return strdup(buf);
}
char *
adb_get_serialno(struct sc_intr *intr, unsigned flags) {
const char *const adb_cmd[] = {"get-serialno"};
sc_pipe pout;
sc_pid pid = adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout);
if (pid == SC_PROCESS_NONE) {
LOGE("Could not execute \"adb get-serialno\"");
return NULL;
}
char buf[128];
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf));
sc_pipe_close(pout);
bool ok = process_check_success_intr(intr, pid, "adb get-serialno", flags);
if (!ok) {
return NULL;
}
if (r == -1) {
return false;
}
sc_str_truncate(buf, r, " \r\n");
return strdup(buf);
}
char *
adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) {
const char *const cmd[] = {"shell", "ip", "route"};
sc_pipe pout;
sc_pid pid = adb_execute_p(serial, cmd, ARRAY_LEN(cmd), flags, &pout);
if (pid == SC_PROCESS_NONE) {
LOGD("Could not execute \"ip route\"");
return NULL;
}
char buf[128]; // Large enough to contain "xxx.xxx.xxx.xxx"
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf));
sc_pipe_close(pout);
bool ok = process_check_success_intr(intr, pid, "ip route", flags);
if (!ok) {
return NULL;
}
if (r == -1) {
return false;
}
sc_str_truncate(buf, r, "\r\n");
// The result looks like
// "192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.x.x"
ssize_t idx = sc_str_index_of_column(buf, 8, " ");
if (idx == -1) {
return NULL;
}
char *ip = &buf[idx];
sc_str_truncate(ip, r - idx, " \t");
return strdup(ip);
}

View File

@ -6,37 +6,89 @@
#include <stdbool.h>
#include <inttypes.h>
#include "util/process.h"
#include "util/intr.h"
#define SC_ADB_NO_STDOUT (1 << 0)
#define SC_ADB_NO_STDERR (1 << 1)
#define SC_ADB_NO_LOGERR (1 << 2)
#define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR)
sc_pid
adb_execute(const char *serial, const char *const adb_cmd[], size_t len);
adb_execute(const char *serial, const char *const adb_cmd[], size_t len,
unsigned flags);
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);
bool
adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port,
const char *device_socket_name, unsigned flags);
sc_pid
adb_forward(const char *serial, uint16_t local_port,
const char *device_socket_name);
bool
adb_forward_remove(struct sc_intr *intr, const char *serial,
uint16_t local_port, unsigned flags);
sc_pid
adb_forward_remove(const char *serial, uint16_t local_port);
bool
adb_reverse(struct sc_intr *intr, const char *serial,
const char *device_socket_name, uint16_t local_port,
unsigned flags);
sc_pid
adb_reverse(const char *serial, const char *device_socket_name,
uint16_t local_port);
bool
adb_reverse_remove(struct sc_intr *intr, const char *serial,
const char *device_socket_name, unsigned flags);
sc_pid
adb_reverse_remove(const char *serial, const char *device_socket_name);
bool
adb_push(struct sc_intr *intr, const char *serial, const char *local,
const char *remote, unsigned flags);
sc_pid
adb_push(const char *serial, const char *local, const char *remote);
bool
adb_install(struct sc_intr *intr, const char *serial, const char *local,
unsigned flags);
sc_pid
adb_install(const char *serial, const char *local);
/**
* Execute `adb tcpip <port>`
*/
bool
adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port,
unsigned flags);
// Return the result of "adb get-serialno".
/**
* Execute `adb connect <ip_port>`
*
* `ip_port` may not be NULL.
*/
bool
adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags);
/**
* Execute `adb disconnect [<ip_port>]`
*
* If `ip_port` is NULL, execute `adb disconnect`.
* Otherwise, execute `adb disconnect <ip_port>`.
*/
bool
adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags);
/**
* Execute `adb getprop <prop>`
*/
char *
adb_get_serialno(void);
adb_getprop(struct sc_intr *intr, const char *serial, const char *prop,
unsigned flags);
/**
* Execute `adb get-serialno`
*
* Return the result, to be freed by the caller, or NULL on error.
*/
char *
adb_get_serialno(struct sc_intr *intr, unsigned flags);
/**
* Attempt to retrieve the device IP
*
* Return the IP as a string of the form "xxx.xxx.xxx.xxx", to be freed by the
* caller, or NULL on error.
*/
char *
adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags);
#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,8 @@ 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,
SC_ADB_NO_STDOUT)) {
// the command itself failed, it will fail on any port
return false;
}
@ -78,7 +52,8 @@ 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,
SC_ADB_NO_STDOUT)) {
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
}
@ -108,7 +83,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, SC_ADB_NO_STDOUT)) {
// success
tunnel->local_port = port;
tunnel->enabled = true;
@ -173,9 +148,11 @@ 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,
SC_ADB_NO_STDOUT);
} else {
ret = disable_tunnel_reverse(intr, serial);
ret = adb_reverse_remove(intr, serial, SC_SOCKET_NAME,
SC_ADB_NO_STDOUT);
assert(tunnel->server_socket != SC_SOCKET_NONE);
if (!net_close(tunnel->server_socket)) {

View File

@ -4,6 +4,7 @@
#include <stdio.h>
#include "aoa_hid.h"
#include "util/log.h"
// See <https://source.android.com/devices/accessories/aoa2#hid-support>.
#define ACCESSORY_REGISTER_HID 54
@ -20,6 +21,7 @@ sc_hid_event_log(const struct sc_hid_event *event) {
unsigned buffer_size = event->size * 3 + 1;
char *buffer = malloc(buffer_size);
if (!buffer) {
LOG_OOM();
return;
}
for (unsigned i = 0; i < event->size; ++i) {
@ -35,7 +37,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 +120,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 +160,7 @@ sc_aoa_init(struct sc_aoa *aoa, const char *serial) {
}
aoa->stopped = false;
aoa->acksync = acksync;
return true;
}
@ -332,23 +338,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 +388,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,11 @@
#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
#define OPT_TCPIP 1033
#define OPT_RAW_KEY_EVENTS 1034
struct sc_option {
char shortopt;
@ -160,7 +166,15 @@ static const struct sc_option options[] = {
"generate non-ASCII characters, contrary to the default "
"injection method.\n"
"It may only work over USB, and is currently only supported "
"on Linux.",
"on Linux.\n"
"The keyboard layout must be configured (once and for all) on "
"the device, via Settings -> System -> Languages and input -> "
"Physical keyboard. This settings page can be started "
"directly:\n"
" adb shell am start -a "
" android.settings.HARD_KEYBOARD_SETTINGS\n"
"However, the option is only available when the HID keyboard "
"is enabled (or a physical keyboard is connected)."
},
{
.shortopt = 'h',
@ -205,6 +219,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",
@ -261,6 +284,11 @@ static const struct sc_option options[] = {
"drag & drop. It is passed as is to \"adb push\".\n"
"Default is \"/sdcard/Download/\".",
},
{
.longopt_id = OPT_RAW_KEY_EVENTS,
.longopt = "raw-key-events",
.text = "Inject key events for all input keys, and ignore text events."
},
{
.shortopt = 'r',
.longopt = "record",
@ -330,6 +358,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,
@ -372,6 +419,20 @@ static const struct sc_option options[] = {
.text = "Keep the device on while scrcpy is running, when the device "
"is plugged in.",
},
{
.longopt_id = OPT_TCPIP,
.longopt = "tcpip",
.argdesc = "ip[:port]",
.optional_arg = true,
.text = "Configure and reconnect the device over TCP/IP.\n"
"If a destination address is provided, then scrcpy connects to "
"this address before starting. The device must listen on the "
"given TCP port (default is 5555).\n"
"If no destination address is provided, then scrcpy attempts "
"to find the IP address of the current device (typically "
"connected over USB), enables TCP/IP mode, then connects to "
"this address before starting.\n",
},
{
.longopt_id = OPT_WINDOW_BORDERLESS,
.longopt = "window-borderless",
@ -563,12 +624,17 @@ sc_getopt_adapter_create_longopts(void) {
struct option *longopts =
malloc((ARRAY_LEN(options) + 1) * sizeof(*longopts));
if (!longopts) {
LOG_OOM();
return NULL;
}
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 +737,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 +758,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 +773,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 +787,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 +825,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 +1189,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 +1276,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;
@ -1266,7 +1357,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->push_target = optarg;
break;
case OPT_PREFER_TEXT:
opts->prefer_text = true;
if (opts->key_inject_mode != SC_KEY_INJECT_MODE_MIXED) {
LOGE("--prefer-text is incompatible with --raw-key-events");
return false;
}
opts->key_inject_mode = SC_KEY_INJECT_MODE_TEXT;
break;
case OPT_RAW_KEY_EVENTS:
if (opts->key_inject_mode != SC_KEY_INJECT_MODE_MIXED) {
LOGE("--prefer-text is incompatible with --raw-key-events");
return false;
}
opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW;
break;
case OPT_ROTATION:
if (!parse_rotation(optarg, &opts->rotation)) {
@ -1313,6 +1415,13 @@ 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;
case OPT_TCPIP:
opts->tcpip = true;
opts->tcpip_dst = optarg;
break;
#ifdef HAVE_V4L2
case OPT_V4L2_SINK:
opts->v4l2_device = optarg;
@ -1329,6 +1438,20 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
}
int index = optind;
if (index < argc) {
LOGE("Unexpected additional argument: %s", argv[index]);
return false;
}
// If a TCP/IP address is provided, then tcpip must be enabled
assert(opts->tcpip || !opts->tcpip_dst);
if (opts->serial && opts->tcpip_dst) {
LOGE("Incompatible options: -s/--serial and --tcpip with an argument");
return false;
}
#ifdef HAVE_V4L2
if (!opts->display && !opts->record_filename && !opts->v4l2_device) {
LOGE("-N/--no-display requires either screen recording (-r/--record)"
@ -1354,10 +1477,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
#endif
int index = optind;
if (index < argc) {
LOGE("Unexpected additional argument: %s", argv[index]);
return false;
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;
}
if (opts->record_format && !opts->record_filename) {

View File

@ -2,6 +2,12 @@
#include "config.h"
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#ifndef HAVE_STRDUP
char *strdup(const char *s) {
size_t size = strlen(s) + 1;
@ -12,3 +18,36 @@ char *strdup(const char *s) {
return dup;
}
#endif
#ifndef HAVE_ASPRINTF
int asprintf(char **strp, const char *fmt, ...) {
va_list va;
va_start(va, fmt);
int ret = vasprintf(strp, fmt, va);
va_end(va);
return ret;
}
#endif
#ifndef HAVE_VASPRINTF
int vasprintf(char **strp, const char *fmt, va_list ap) {
va_list va;
va_copy(va, ap);
int len = vsnprintf(NULL, 0, fmt, va);
va_end(va);
char *str = malloc(len + 1);
if (!str) {
return -1;
}
va_copy(va, ap);
int len2 = vsnprintf(str, len + 1, fmt, va);
(void) len2;
assert(len == len2);
va_end(va);
*strp = str;
return len;
}
#endif

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(),
@ -57,4 +58,12 @@
char *strdup(const char *s);
#endif
#ifndef HAVE_ASPRINTF
int asprintf(char **strp, const char *fmt, ...);
#endif
#ifndef HAVE_VASPRINTF
int vasprintf(char **strp, const char *fmt, va_list ap);
#endif
#endif

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

@ -42,7 +42,7 @@ static bool
decoder_open(struct decoder *decoder, const AVCodec *codec) {
decoder->codec_ctx = avcodec_alloc_context3(codec);
if (!decoder->codec_ctx) {
LOGC("Could not allocate decoder context");
LOG_OOM();
return false;
}
@ -54,7 +54,7 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) {
decoder->frame = av_frame_alloc();
if (!decoder->frame) {
LOGE("Could not create decoder frame");
LOG_OOM();
avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx);
return false;

View File

@ -1,5 +1,6 @@
#include "device_msg.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@ -23,7 +24,7 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
}
char *text = malloc(clipboard_len + 1);
if (!text) {
LOGW("Could not allocate text for clipboard");
LOG_OOM();
return -1;
}
if (clipboard_len) {
@ -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) {
sc_cond_destroy(&file_handler->event_cond);
sc_mutex_destroy(&file_handler->mutex);
return false;
}
file_handler->serial = strdup(serial);
if (!file_handler->serial) {
LOG_OOM();
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, 0);
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, 0);
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

@ -10,11 +10,13 @@ bool
sc_frame_buffer_init(struct sc_frame_buffer *fb) {
fb->pending_frame = av_frame_alloc();
if (!fb->pending_frame) {
LOG_OOM();
return false;
}
fb->tmp_frame = av_frame_alloc();
if (!fb->tmp_frame) {
LOG_OOM();
av_frame_free(&fb->pending_frame);
return false;
}

View File

@ -160,6 +160,7 @@ static bool
sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) {
unsigned char *buffer = malloc(HID_KEYBOARD_EVENT_SIZE);
if (!buffer) {
LOG_OOM();
return false;
}
@ -278,7 +279,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 +300,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 +346,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

@ -31,7 +31,7 @@ get_icon_path(void) {
char *icon_path = strdup(icon_path_env);
#endif
if (!icon_path) {
LOGE("Could not allocate memory");
LOG_OOM();
return NULL;
}
LOGD("Using SCRCPY_ICON_PATH: %s", icon_path);
@ -42,7 +42,7 @@ get_icon_path(void) {
LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH);
char *icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH);
if (!icon_path) {
LOGE("Could not allocate memory");
LOG_OOM();
return NULL;
}
#else
@ -63,7 +63,7 @@ decode_image(const char *path) {
AVFormatContext *ctx = avformat_alloc_context();
if (!ctx) {
LOGE("Could not allocate image decoder context");
LOG_OOM();
return NULL;
}
@ -93,7 +93,7 @@ decode_image(const char *path) {
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
LOGE("Could not allocate codec context");
LOG_OOM();
goto close_input;
}
@ -109,13 +109,13 @@ decode_image(const char *path) {
AVFrame *frame = av_frame_alloc();
if (!frame) {
LOGE("Could not allocate frame");
LOG_OOM();
goto close_codec;
}
AVPacket *packet = av_packet_alloc();
if (!packet) {
LOGE("Could not allocate packet");
LOG_OOM();
av_frame_free(&frame);
goto close_codec;
}

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

@ -6,65 +6,168 @@
#include "android/input.h"
#include "control_msg.h"
#include "controller.h"
#include "util/intmap.h"
#include "util/log.h"
/** Downcast key processor to sc_keyboard_inject */
#define DOWNCAST(KP) \
container_of(KP, struct sc_keyboard_inject, key_processor)
#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_inject, key_processor)
#define MAP(FROM, TO) case FROM: *to = TO; return true
#define FAIL default: return false
static bool
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
switch (from) {
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP);
FAIL;
static const struct sc_intmap_entry actions[] = {
{SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN},
{SDL_KEYUP, AKEY_EVENT_ACTION_UP},
};
const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from);
if (entry) {
*to = entry->value;
return true;
}
return false;
}
static bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
bool prefer_text) {
switch (from) {
MAP(SDLK_RETURN, AKEYCODE_ENTER);
MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER);
MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE);
MAP(SDLK_BACKSPACE, AKEYCODE_DEL);
MAP(SDLK_TAB, AKEYCODE_TAB);
MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP);
MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL);
MAP(SDLK_HOME, AKEYCODE_MOVE_HOME);
MAP(SDLK_END, AKEYCODE_MOVE_END);
MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN);
MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT);
MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT);
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
MAP(SDLK_UP, AKEYCODE_DPAD_UP);
MAP(SDLK_LCTRL, AKEYCODE_CTRL_LEFT);
MAP(SDLK_RCTRL, AKEYCODE_CTRL_RIGHT);
MAP(SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT);
MAP(SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT);
enum sc_key_inject_mode key_inject_mode) {
// Navigation keys and ENTER.
// Used in all modes.
static const struct sc_intmap_entry special_keys[] = {
{SDLK_RETURN, AKEYCODE_ENTER},
{SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER},
{SDLK_ESCAPE, AKEYCODE_ESCAPE},
{SDLK_BACKSPACE, AKEYCODE_DEL},
{SDLK_TAB, AKEYCODE_TAB},
{SDLK_PAGEUP, AKEYCODE_PAGE_UP},
{SDLK_DELETE, AKEYCODE_FORWARD_DEL},
{SDLK_HOME, AKEYCODE_MOVE_HOME},
{SDLK_END, AKEYCODE_MOVE_END},
{SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN},
{SDLK_RIGHT, AKEYCODE_DPAD_RIGHT},
{SDLK_LEFT, AKEYCODE_DPAD_LEFT},
{SDLK_DOWN, AKEYCODE_DPAD_DOWN},
{SDLK_UP, AKEYCODE_DPAD_UP},
{SDLK_LCTRL, AKEYCODE_CTRL_LEFT},
{SDLK_RCTRL, AKEYCODE_CTRL_RIGHT},
{SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT},
{SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT},
};
// Numpad navigation keys.
// Used in all modes, when NumLock and Shift are disabled.
static const struct sc_intmap_entry kp_nav_keys[] = {
{SDLK_KP_0, AKEYCODE_INSERT},
{SDLK_KP_1, AKEYCODE_MOVE_END},
{SDLK_KP_2, AKEYCODE_DPAD_DOWN},
{SDLK_KP_3, AKEYCODE_PAGE_DOWN},
{SDLK_KP_4, AKEYCODE_DPAD_LEFT},
{SDLK_KP_6, AKEYCODE_DPAD_RIGHT},
{SDLK_KP_7, AKEYCODE_MOVE_HOME},
{SDLK_KP_8, AKEYCODE_DPAD_UP},
{SDLK_KP_9, AKEYCODE_PAGE_UP},
{SDLK_KP_PERIOD, AKEYCODE_FORWARD_DEL},
};
// Letters and space.
// Used in non-text mode.
static const struct sc_intmap_entry alphaspace_keys[] = {
{SDLK_a, AKEYCODE_A},
{SDLK_b, AKEYCODE_B},
{SDLK_c, AKEYCODE_C},
{SDLK_d, AKEYCODE_D},
{SDLK_e, AKEYCODE_E},
{SDLK_f, AKEYCODE_F},
{SDLK_g, AKEYCODE_G},
{SDLK_h, AKEYCODE_H},
{SDLK_i, AKEYCODE_I},
{SDLK_j, AKEYCODE_J},
{SDLK_k, AKEYCODE_K},
{SDLK_l, AKEYCODE_L},
{SDLK_m, AKEYCODE_M},
{SDLK_n, AKEYCODE_N},
{SDLK_o, AKEYCODE_O},
{SDLK_p, AKEYCODE_P},
{SDLK_q, AKEYCODE_Q},
{SDLK_r, AKEYCODE_R},
{SDLK_s, AKEYCODE_S},
{SDLK_t, AKEYCODE_T},
{SDLK_u, AKEYCODE_U},
{SDLK_v, AKEYCODE_V},
{SDLK_w, AKEYCODE_W},
{SDLK_x, AKEYCODE_X},
{SDLK_y, AKEYCODE_Y},
{SDLK_z, AKEYCODE_Z},
{SDLK_SPACE, AKEYCODE_SPACE},
};
// Numbers and punctuation keys.
// Used in raw mode only.
static const struct sc_intmap_entry numbers_punct_keys[] = {
{SDLK_HASH, AKEYCODE_POUND},
{SDLK_PERCENT, AKEYCODE_PERIOD},
{SDLK_QUOTE, AKEYCODE_APOSTROPHE},
{SDLK_ASTERISK, AKEYCODE_STAR},
{SDLK_PLUS, AKEYCODE_PLUS},
{SDLK_COMMA, AKEYCODE_COMMA},
{SDLK_MINUS, AKEYCODE_MINUS},
{SDLK_PERIOD, AKEYCODE_PERIOD},
{SDLK_SLASH, AKEYCODE_SLASH},
{SDLK_0, AKEYCODE_0},
{SDLK_1, AKEYCODE_1},
{SDLK_2, AKEYCODE_2},
{SDLK_3, AKEYCODE_3},
{SDLK_4, AKEYCODE_4},
{SDLK_5, AKEYCODE_5},
{SDLK_6, AKEYCODE_6},
{SDLK_7, AKEYCODE_7},
{SDLK_8, AKEYCODE_8},
{SDLK_8, AKEYCODE_9},
{SDLK_SEMICOLON, AKEYCODE_SEMICOLON},
{SDLK_EQUALS, AKEYCODE_EQUALS},
{SDLK_AT, AKEYCODE_AT},
{SDLK_LEFTBRACKET, AKEYCODE_LEFT_BRACKET},
{SDLK_BACKSLASH, AKEYCODE_BACKSLASH},
{SDLK_RIGHTBRACKET, AKEYCODE_RIGHT_BRACKET},
{SDLK_BACKQUOTE, AKEYCODE_GRAVE},
{SDLK_KP_1, AKEYCODE_NUMPAD_1},
{SDLK_KP_2, AKEYCODE_NUMPAD_2},
{SDLK_KP_3, AKEYCODE_NUMPAD_3},
{SDLK_KP_4, AKEYCODE_NUMPAD_4},
{SDLK_KP_5, AKEYCODE_NUMPAD_5},
{SDLK_KP_6, AKEYCODE_NUMPAD_6},
{SDLK_KP_7, AKEYCODE_NUMPAD_7},
{SDLK_KP_8, AKEYCODE_NUMPAD_8},
{SDLK_KP_9, AKEYCODE_NUMPAD_9},
{SDLK_KP_0, AKEYCODE_NUMPAD_0},
{SDLK_KP_DIVIDE, AKEYCODE_NUMPAD_DIVIDE},
{SDLK_KP_MULTIPLY, AKEYCODE_NUMPAD_MULTIPLY},
{SDLK_KP_MINUS, AKEYCODE_NUMPAD_SUBTRACT},
{SDLK_KP_PLUS, AKEYCODE_NUMPAD_ADD},
{SDLK_KP_PERIOD, AKEYCODE_NUMPAD_DOT},
{SDLK_KP_EQUALS, AKEYCODE_NUMPAD_EQUALS},
{SDLK_KP_LEFTPAREN, AKEYCODE_NUMPAD_LEFT_PAREN},
{SDLK_KP_RIGHTPAREN, AKEYCODE_NUMPAD_RIGHT_PAREN},
};
const struct sc_intmap_entry *entry =
SC_INTMAP_FIND_ENTRY(special_keys, from);
if (entry) {
*to = entry->value;
return true;
}
if (!(mod & (KMOD_NUM | KMOD_SHIFT))) {
// Handle Numpad events when Num Lock is disabled
// If SHIFT is pressed, a text event will be sent instead
switch(from) {
MAP(SDLK_KP_0, AKEYCODE_INSERT);
MAP(SDLK_KP_1, AKEYCODE_MOVE_END);
MAP(SDLK_KP_2, AKEYCODE_DPAD_DOWN);
MAP(SDLK_KP_3, AKEYCODE_PAGE_DOWN);
MAP(SDLK_KP_4, AKEYCODE_DPAD_LEFT);
MAP(SDLK_KP_6, AKEYCODE_DPAD_RIGHT);
MAP(SDLK_KP_7, AKEYCODE_MOVE_HOME);
MAP(SDLK_KP_8, AKEYCODE_DPAD_UP);
MAP(SDLK_KP_9, AKEYCODE_PAGE_UP);
MAP(SDLK_KP_PERIOD, AKEYCODE_FORWARD_DEL);
entry = SC_INTMAP_FIND_ENTRY(kp_nav_keys, from);
if (entry) {
*to = entry->value;
return true;
}
}
if (prefer_text && !(mod & KMOD_CTRL)) {
if (key_inject_mode == SC_KEY_INJECT_MODE_TEXT && !(mod & KMOD_CTRL)) {
// do not forward alpha and space key events (unless Ctrl is pressed)
return false;
}
@ -72,37 +175,23 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) {
return false;
}
// if ALT and META are not pressed, also handle letters and space
switch (from) {
MAP(SDLK_a, AKEYCODE_A);
MAP(SDLK_b, AKEYCODE_B);
MAP(SDLK_c, AKEYCODE_C);
MAP(SDLK_d, AKEYCODE_D);
MAP(SDLK_e, AKEYCODE_E);
MAP(SDLK_f, AKEYCODE_F);
MAP(SDLK_g, AKEYCODE_G);
MAP(SDLK_h, AKEYCODE_H);
MAP(SDLK_i, AKEYCODE_I);
MAP(SDLK_j, AKEYCODE_J);
MAP(SDLK_k, AKEYCODE_K);
MAP(SDLK_l, AKEYCODE_L);
MAP(SDLK_m, AKEYCODE_M);
MAP(SDLK_n, AKEYCODE_N);
MAP(SDLK_o, AKEYCODE_O);
MAP(SDLK_p, AKEYCODE_P);
MAP(SDLK_q, AKEYCODE_Q);
MAP(SDLK_r, AKEYCODE_R);
MAP(SDLK_s, AKEYCODE_S);
MAP(SDLK_t, AKEYCODE_T);
MAP(SDLK_u, AKEYCODE_U);
MAP(SDLK_v, AKEYCODE_V);
MAP(SDLK_w, AKEYCODE_W);
MAP(SDLK_x, AKEYCODE_X);
MAP(SDLK_y, AKEYCODE_Y);
MAP(SDLK_z, AKEYCODE_Z);
MAP(SDLK_SPACE, AKEYCODE_SPACE);
FAIL;
entry = SC_INTMAP_FIND_ENTRY(alphaspace_keys, from);
if (entry) {
*to = entry->value;
return true;
}
if (key_inject_mode == SC_KEY_INJECT_MODE_RAW) {
entry = SC_INTMAP_FIND_ENTRY(numbers_punct_keys, from);
if (entry) {
*to = entry->value;
return true;
}
}
return false;
}
static enum android_metastate
@ -167,7 +256,7 @@ convert_meta_state(SDL_Keymod mod) {
static bool
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
bool prefer_text, uint32_t repeat) {
enum sc_key_inject_mode key_inject_mode, uint32_t repeat) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
@ -176,7 +265,7 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
uint16_t mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod,
prefer_text)) {
key_inject_mode)) {
return false;
}
@ -188,7 +277,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) {
@ -201,7 +296,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp,
}
struct control_msg msg;
if (convert_input_key(event, &msg, ki->prefer_text, ki->repeat)) {
if (convert_input_key(event, &msg, ki->key_inject_mode, ki->repeat)) {
if (!controller_push_msg(ki->controller, &msg)) {
LOGW("Could not request 'inject keycode'");
}
@ -213,11 +308,16 @@ sc_key_processor_process_text(struct sc_key_processor *kp,
const SDL_TextInputEvent *event) {
struct sc_keyboard_inject *ki = DOWNCAST(kp);
if (!ki->prefer_text) {
if (ki->key_inject_mode == SC_KEY_INJECT_MODE_RAW) {
// Never inject text events
return;
}
if (ki->key_inject_mode == SC_KEY_INJECT_MODE_MIXED) {
char c = event->text[0];
if (isalpha(c) || c == ' ') {
assert(event->text[1] == '\0');
// letters and space are handled as raw key event
// Letters and space are handled as raw key events
return;
}
}
@ -240,7 +340,7 @@ sc_keyboard_inject_init(struct sc_keyboard_inject *ki,
struct controller *controller,
const struct scrcpy_options *options) {
ki->controller = controller;
ki->prefer_text = options->prefer_text;
ki->key_inject_mode = options->key_inject_mode;
ki->forward_key_repeat = options->forward_key_repeat;
ki->repeat = 0;
@ -250,5 +350,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

@ -18,7 +18,7 @@ struct sc_keyboard_inject {
// number of repetitions. This variable keeps track of the count.
unsigned repeat;
bool prefer_text;
enum sc_key_inject_mode key_inject_mode;
bool forward_key_repeat;
};

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

@ -6,6 +6,7 @@
#include "android/input.h"
#include "control_msg.h"
#include "controller.h"
#include "util/intmap.h"
#include "util/log.h"
/** Downcast mouse processor to sc_mouse_inject */
@ -32,25 +33,37 @@ convert_mouse_buttons(uint32_t state) {
return buttons;
}
#define MAP(FROM, TO) case FROM: *to = TO; return true
#define FAIL default: return false
static bool
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) {
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP);
FAIL;
static const struct sc_intmap_entry actions[] = {
{SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN},
{SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP},
};
const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from);
if (entry) {
*to = entry->value;
return true;
}
return false;
}
static bool
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) {
MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE);
MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN);
MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP);
FAIL;
static const struct sc_intmap_entry actions[] = {
{SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE},
{SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN},
{SDL_FINGERUP, AMOTION_EVENT_ACTION_UP},
};
const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from);
if (entry) {
*to = entry->value;
return true;
}
return false;
}
static bool

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,
@ -41,7 +43,7 @@ const struct scrcpy_options scrcpy_options_default = {
.control = true,
.display = true,
.turn_screen_off = false,
.prefer_text = false,
.key_inject_mode = SC_KEY_INJECT_MODE_MIXED,
.window_borderless = false,
.mipmaps = true,
.stay_awake = false,
@ -51,4 +53,7 @@ const struct scrcpy_options scrcpy_options_default = {
.forward_all_clicks = false,
.legacy_paste = false,
.power_off_on_close = false,
.clipboard_autosync = true,
.tcpip = false,
.tcpip_dst = NULL,
};

View File

@ -38,6 +38,20 @@ enum sc_keyboard_input_mode {
SC_KEYBOARD_INPUT_MODE_HID,
};
enum sc_key_inject_mode {
// Inject special keys, letters and space as key events;
// inject numbers and punctuation as text events.
// This is the default mode.
SC_KEY_INJECT_MODE_MIXED,
// Inject special keys as key events,
// inject letters and space, numbers and punctuation as text events.
SC_KEY_INJECT_MODE_TEXT,
// Inject everything as key events.
SC_KEY_INJECT_MODE_RAW,
};
#define SC_MAX_SHORTCUT_MODS 8
enum sc_shortcut_mod {
@ -77,6 +91,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;
@ -96,7 +112,7 @@ struct scrcpy_options {
bool control;
bool display;
bool turn_screen_off;
bool prefer_text;
enum sc_key_inject_mode key_inject_mode;
bool window_borderless;
bool mipmaps;
bool stay_awake;
@ -106,6 +122,9 @@ struct scrcpy_options {
bool forward_all_clicks;
bool legacy_paste;
bool power_off_on_close;
bool clipboard_autosync;
bool tcpip;
const char *tcpip_dst;
};
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

@ -34,11 +34,13 @@ static struct record_packet *
record_packet_new(const AVPacket *packet) {
struct record_packet *rec = malloc(sizeof(*rec));
if (!rec) {
LOG_OOM();
return NULL;
}
rec->packet = av_packet_alloc();
if (!rec->packet) {
LOG_OOM();
free(rec);
return NULL;
}
@ -81,7 +83,7 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) {
LOGC("Could not allocate extradata");
LOG_OOM();
return false;
}
@ -228,13 +230,11 @@ static bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
bool ok = sc_mutex_init(&recorder->mutex);
if (!ok) {
LOGC("Could not create mutex");
return false;
}
ok = sc_cond_init(&recorder->queue_cond);
if (!ok) {
LOGC("Could not create cond");
goto error_mutex_destroy;
}
@ -254,7 +254,7 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
recorder->ctx = avformat_alloc_context();
if (!recorder->ctx) {
LOGE("Could not allocate output context");
LOG_OOM();
goto error_cond_destroy;
}
@ -338,7 +338,7 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) {
struct record_packet *rec = record_packet_new(packet);
if (!rec) {
LOGC("Could not allocate record packet");
LOG_OOM();
sc_mutex_unlock(&recorder->mutex);
return false;
}
@ -375,7 +375,7 @@ recorder_init(struct recorder *recorder,
struct sc_size declared_frame_size) {
recorder->filename = strdup(filename);
if (!recorder->filename) {
LOGE("Could not strdup filename");
LOG_OOM();
return false;
}

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;
@ -268,7 +271,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
size_t fmt_len = strlen(fmt);
char *local_fmt = malloc(fmt_len + 10);
if (!local_fmt) {
LOGC("Could not allocate string");
LOG_OOM();
return;
}
memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0'
@ -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,9 @@ 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,
.tcpip = options->tcpip,
.tcpip_dst = options->tcpip_dst,
};
static const struct sc_server_callbacks cbs = {
@ -394,8 +404,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 +453,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 +540,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 +605,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

@ -366,18 +366,15 @@ screen_init(struct screen *screen, const struct screen_params *params) {
bool ok = sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs,
screen);
if (!ok) {
LOGE("Could not initialize video buffer");
return false;
}
ok = sc_video_buffer_start(&screen->vb);
if (!ok) {
LOGE("Could not start video_buffer");
goto error_destroy_video_buffer;
}
if (!fps_counter_init(&screen->fps_counter)) {
LOGE("Could not initialize FPS counter");
goto error_stop_and_join_video_buffer;
}
@ -478,7 +475,7 @@ screen_init(struct screen *screen, const struct screen_params *params) {
screen->frame = av_frame_alloc();
if (!screen->frame) {
LOGC("Could not create screen frame");
LOG_OOM();
goto error_destroy_texture;
}

View File

@ -34,7 +34,7 @@ get_server_path(void) {
char *server_path = strdup(server_path_env);
#endif
if (!server_path) {
LOGE("Could not allocate memory");
LOG_OOM();
return NULL;
}
LOGD("Using SCRCPY_SERVER_PATH: %s", server_path);
@ -45,7 +45,7 @@ get_server_path(void) {
LOGD("Using server: " SC_SERVER_PATH_DEFAULT);
char *server_path = strdup(SC_SERVER_PATH_DEFAULT);
if (!server_path) {
LOGE("Could not allocate memory");
LOG_OOM();
return NULL;
}
#else
@ -69,6 +69,7 @@ sc_server_params_destroy(struct sc_server_params *params) {
free((char *) params->crop);
free((char *) params->codec_options);
free((char *) params->encoder_name);
free((char *) params->tcpip_dst);
}
static bool
@ -92,6 +93,7 @@ sc_server_params_copy(struct sc_server_params *dst,
COPY(crop);
COPY(codec_options);
COPY(encoder_name);
COPY(tcpip_dst);
#undef COPY
return true;
@ -112,9 +114,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, 0);
free(server_path);
return sc_process_check_success_intr(intr, pid, "adb push");
return ok;
}
static const char *
@ -136,28 +138,34 @@ log_level_to_server_string(enum sc_log_level level) {
}
}
static bool
sc_server_sleep(struct sc_server *server, sc_tick deadline) {
sc_mutex_lock(&server->mutex);
bool timed_out = false;
while (!server->stopped && !timed_out) {
timed_out = !sc_cond_timedwait(&server->cond_stopped,
&server->mutex, deadline);
}
bool stopped = server->stopped;
sc_mutex_unlock(&server->mutex);
return !stopped;
}
static sc_pid
execute_server(struct sc_server *server,
const struct sc_server_params *params) {
const char *serial = server->params.serial;
sc_pid pid = SC_PROCESS_NONE;
const char *cmd[128];
unsigned count = 0;
cmd[count++] = "shell";
cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH;
cmd[count++] = "app_process";
char max_size_string[6];
char bit_rate_string[11];
char max_fps_string[6];
char lock_video_orientation_string[5];
char display_id_string[11];
sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
sprintf(lock_video_orientation_string, "%"PRIi8,
params->lock_video_orientation);
sprintf(display_id_string, "%"PRIu32, params->display_id);
const char *const cmd[] = {
"shell",
"CLASSPATH=" SC_DEVICE_SERVER_PATH,
"app_process",
#ifdef SERVER_DEBUGGER
# define SERVER_DEBUGGER_PORT "5005"
cmd[count++] =
# ifdef SERVER_DEBUGGER_METHOD_NEW
/* Android 9 and above */
"-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,"
@ -166,27 +174,71 @@ execute_server(struct sc_server *server,
/* Android 8 and below */
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
# endif
SERVER_DEBUGGER_PORT,
SERVER_DEBUGGER_PORT;
#endif
"/", // unused
"com.genymobile.scrcpy.Server",
SCRCPY_VERSION,
log_level_to_server_string(params->log_level),
max_size_string,
bit_rate_string,
max_fps_string,
lock_video_orientation_string,
server->tunnel.forward ? "true" : "false",
params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp)
params->control ? "true" : "false",
display_id_string,
params->show_touches ? "true" : "false",
params->stay_awake ? "true" : "false",
params->codec_options ? params->codec_options : "-",
params->encoder_name ? params->encoder_name : "-",
params->power_off_on_close ? "true" : "false",
};
cmd[count++] = "/"; // unused
cmd[count++] = "com.genymobile.scrcpy.Server";
cmd[count++] = SCRCPY_VERSION;
unsigned dyn_idx = count; // from there, the strings are allocated
#define ADD_PARAM(fmt, ...) { \
char *p = (char *) &cmd[count]; \
if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \
goto end; \
} \
cmd[count++] = p; \
}
#define STRBOOL(v) (v ? "true" : "false")
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size);
}
if (params->max_fps) {
ADD_PARAM("max_fps=%" PRIu16, params->max_fps);
}
if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
ADD_PARAM("lock_video_orientation=%" PRIi8,
params->lock_video_orientation);
}
if (server->tunnel.forward) {
ADD_PARAM("tunnel_forward=%s", STRBOOL(server->tunnel.forward));
}
if (params->crop) {
ADD_PARAM("crop=%s", params->crop);
}
if (!params->control) {
// By default, control is true
ADD_PARAM("control=%s", STRBOOL(params->control));
}
if (params->display_id) {
ADD_PARAM("display_id=%" PRIu32, params->display_id);
}
if (params->show_touches) {
ADD_PARAM("show_touches=%s", STRBOOL(params->show_touches));
}
if (params->stay_awake) {
ADD_PARAM("stay_awake=%s", STRBOOL(params->stay_awake));
}
if (params->codec_options) {
ADD_PARAM("codec_options=%s", params->codec_options);
}
if (params->encoder_name) {
ADD_PARAM("encoder_name=%s", params->encoder_name);
}
if (params->power_off_on_close) {
ADD_PARAM("power_off_on_close=%s", STRBOOL(params->power_off_on_close));
}
if (!params->clipboard_autosync) {
// By defaut, clipboard_autosync is true
ADD_PARAM("clipboard_autosync=%s", STRBOOL(params->clipboard_autosync));
}
#undef ADD_PARAM
#undef STRBOOL
#ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port "
SERVER_DEBUGGER_PORT "...");
@ -198,12 +250,21 @@ execute_server(struct sc_server *server,
// Port: 5005
// Then click on "Debug"
#endif
return adb_execute(serial, cmd, ARRAY_LEN(cmd));
// Inherit both stdout and stderr (all server logs are printed to stdout)
pid = adb_execute(params->serial, cmd, count, 0);
end:
for (unsigned i = dyn_idx; i < count; ++i) {
free((char *) cmd[i]);
}
return pid;
}
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 +281,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, unsigned attempts, sc_tick delay,
uint32_t host, uint16_t port) {
do {
LOGD("Remaining connection attempts: %d", (int) attempts);
LOGD("Remaining connection attempts: %u", 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,23 +295,21 @@ connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay) {
net_close(socket);
}
if (attempts) {
sc_mutex_lock(&server->mutex);
sc_tick deadline = sc_tick_now() + delay;
bool timed_out = false;
while (!server->stopped && !timed_out) {
timed_out = !sc_cond_timedwait(&server->cond_stopped,
&server->mutex, deadline);
}
bool stopped = server->stopped;
sc_mutex_unlock(&server->mutex);
if (stopped) {
if (sc_intr_is_interrupted(&server->intr)) {
// Stop immediately
break;
}
if (attempts) {
sc_tick deadline = sc_tick_now() + delay;
bool ok = sc_server_sleep(server, deadline);
if (!ok) {
LOGI("Connection attempt stopped");
break;
}
}
} while (--attempts > 0);
} while (--attempts);
return SC_SOCKET_NONE;
}
@ -259,20 +318,18 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
const struct sc_server_callbacks *cbs, void *cbs_userdata) {
bool ok = sc_server_params_copy(&server->params, params);
if (!ok) {
LOGE("Could not copy server params");
LOG_OOM();
return false;
}
ok = sc_mutex_init(&server->mutex);
if (!ok) {
LOGE("Could not create server mutex");
sc_server_params_destroy(&server->params);
return false;
}
ok = sc_cond_init(&server->cond_stopped);
if (!ok) {
LOGE("Could not create server cond_stopped");
sc_mutex_destroy(&server->mutex);
sc_server_params_destroy(&server->params);
return false;
@ -280,7 +337,6 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
ok = sc_intr_init(&server->intr);
if (!ok) {
LOGE("Could not create intr");
sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex);
sc_server_params_destroy(&server->params);
@ -346,9 +402,20 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
goto fail;
}
} else {
uint32_t attempts = 100;
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;
}
unsigned 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 +425,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 +483,230 @@ 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, 0);
if (!server->params.serial) {
LOGE("Could not get device serial");
return false;
}
LOGD("Device serial: %s", server->params.serial);
}
return true;
}
static bool
is_tcpip_mode_enabled(struct sc_server *server) {
struct sc_intr *intr = &server->intr;
const char *serial = server->params.serial;
char *current_port =
adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT);
if (!current_port) {
return false;
}
// Is the device is listening on TCP on port 5555
bool enabled = !strcmp("5555", current_port);
free(current_port);
return enabled;
}
static bool
wait_tcpip_mode_enabled(struct sc_server *server, unsigned attempts,
sc_tick delay) {
if (is_tcpip_mode_enabled(server)) {
LOGI("TCP/IP mode enabled");
return true;
}
// Only print this log if TCP/IP is not enabled
LOGI("Waiting for TCP/IP mode enabled...");
do {
sc_tick deadline = sc_tick_now() + delay;
if (!sc_server_sleep(server, deadline)) {
LOGI("TCP/IP mode waiting interrupted");
return false;
}
if (is_tcpip_mode_enabled(server)) {
LOGI("TCP/IP mode enabled");
return true;
}
} while (--attempts);
return false;
}
char *
append_port_5555(const char *ip) {
size_t len = strlen(ip);
// sizeof counts the final '\0'
char *ip_port = malloc(len + sizeof(":5555"));
if (!ip_port) {
LOG_OOM();
return NULL;
}
memcpy(ip_port, ip, len);
memcpy(ip_port + len, ":5555", sizeof(":5555"));
return ip_port;
}
static bool
sc_server_switch_to_tcpip(struct sc_server *server, char **out_ip_port) {
const char *serial = server->params.serial;
assert(serial);
struct sc_intr *intr = &server->intr;
char *ip = adb_get_device_ip(intr, serial, 0);
if (!ip) {
LOGE("Device IP not found");
return false;
}
char *ip_port = append_port_5555(ip);
free(ip);
if (!ip_port) {
return false;
}
bool tcp_mode = is_tcpip_mode_enabled(server);
if (!tcp_mode) {
bool ok = adb_tcpip(intr, serial, 5555, SC_ADB_NO_STDOUT);
if (!ok) {
LOGE("Could not restart adbd in TCP/IP mode");
goto error;
}
unsigned attempts = 40;
sc_tick delay = SC_TICK_FROM_MS(250);
ok = wait_tcpip_mode_enabled(server, attempts, delay);
if (!ok) {
goto error;
}
}
*out_ip_port = ip_port;
return true;
error:
free(ip_port);
return false;
}
static bool
sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) {
struct sc_intr *intr = &server->intr;
// Error expected if not connected, do not report any error
adb_disconnect(intr, ip_port, SC_ADB_SILENT);
bool ok = adb_connect(intr, ip_port, 0);
if (!ok) {
LOGE("Could not connect to %s", ip_port);
return false;
}
// Override the serial, owned by the sc_server_params
free((void *) server->params.serial);
server->params.serial = strdup(ip_port);
if (!server->params.serial) {
LOG_OOM();
return false;
}
LOGI("Connected to %s", ip_port);
return true;
}
static bool
sc_server_configure_tcpip(struct sc_server *server) {
char *ip_port;
const struct sc_server_params *params = &server->params;
// If tcpip parameter is given, then it must connect to this address.
// Therefore, the device is unknown, so serial is meaningless at this point.
assert(!params->serial || !params->tcpip_dst);
if (params->tcpip_dst) {
// Append ":5555" if no port is present
bool contains_port = strchr(params->tcpip_dst, ':');
ip_port = contains_port ? strdup(params->tcpip_dst)
: append_port_5555(params->tcpip_dst);
if (!ip_port) {
LOG_OOM();
return false;
}
} else {
// The device IP address must be retrieved from the current
// connected device
if (!sc_server_fill_serial(server)) {
return false;
}
// The serial is either the real serial when connected via USB, or
// the IP:PORT when connected over TCP/IP. Only the latter contains
// a colon.
bool is_already_tcpip = strchr(params->serial, ':');
if (is_already_tcpip) {
// Nothing to do
LOGI("Device is already connected via TCP/IP: %s",
params->serial);
return true;
}
bool ok = sc_server_switch_to_tcpip(server, &ip_port);
if (!ok) {
return false;
}
}
// On success, this call changes params->serial
bool ok = sc_server_connect_to_tcpip(server, ip_port);
free(ip_port);
return ok;
}
static int
run_server(void *data) {
struct sc_server *server = data;
const struct sc_server_params *params = &server->params;
if (params->serial) {
LOGD("Device serial: %s", params->serial);
}
if (params->tcpip) {
// params->serial may be changed after this call
bool ok = sc_server_configure_tcpip(server);
if (!ok) {
goto error_connection_failed;
}
}
// It is ok to call this function even if the device serial has been
// changed to switch over TCP/IP
if (!sc_server_fill_serial(server)) {
goto error_connection_failed;
}
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,9 @@ struct sc_server_params {
bool stay_awake;
bool force_adb_forward;
bool power_off_on_close;
bool clipboard_autosync;
bool tcpip;
const char *tcpip_dst;
};
struct sc_server {

View File

@ -42,7 +42,7 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) {
assert(len);
if (av_new_packet(packet, len)) {
LOGE("Could not allocate packet");
LOG_OOM();
return false;
}
@ -111,18 +111,18 @@ stream_push_packet(struct stream *stream, AVPacket *packet) {
if (stream->pending) {
offset = stream->pending->size;
if (av_grow_packet(stream->pending, packet->size)) {
LOGE("Could not grow packet");
LOG_OOM();
return false;
}
} else {
offset = 0;
stream->pending = av_packet_alloc();
if (!stream->pending) {
LOGE("Could not allocate packet");
LOG_OOM();
return false;
}
if (av_new_packet(stream->pending, packet->size)) {
LOGE("Could not create packet");
LOG_OOM();
av_packet_free(&stream->pending);
return false;
}
@ -200,7 +200,7 @@ run_stream(void *data) {
stream->codec_ctx = avcodec_alloc_context3(codec);
if (!stream->codec_ctx) {
LOGC("Could not allocate codec context");
LOG_OOM();
goto end;
}
@ -221,7 +221,7 @@ run_stream(void *data) {
AVPacket *packet = av_packet_alloc();
if (!packet) {
LOGE("Could not allocate packet");
LOG_OOM();
goto finally_close_parser;
}

View File

@ -2,10 +2,13 @@
#include <limits.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "util/log.h"
bool
sc_file_executable_exists(const char *file) {
char *path = getenv("PATH");
@ -23,7 +26,10 @@ sc_file_executable_exists(const char *file) {
size_t dir_len = strlen(dir);
char *fullpath = malloc(dir_len + file_len + 2);
if (!fullpath)
{
LOG_OOM();
continue;
}
memcpy(fullpath, dir, dir_len);
fullpath[dir_len] = '/';
memcpy(fullpath + dir_len + 1, file, file_len + 1);

View File

@ -11,8 +11,11 @@
#include "util/log.h"
enum sc_process_result
sc_process_execute_p(const char *const argv[], sc_pid *pid,
sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags,
int *pin, int *pout, int *perr) {
bool inherit_stdout = !pout && !(flags & SC_PROCESS_NO_STDOUT);
bool inherit_stderr = !perr && !(flags & SC_PROCESS_NO_STDERR);
int in[2];
int out[2];
int err[2];
@ -90,20 +93,30 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid,
}
close(in[1]);
}
// Do not close stdin in the child process, this makes adb fail on Linux
if (pout) {
if (out[1] != STDOUT_FILENO) {
dup2(out[1], STDOUT_FILENO);
close(out[1]);
}
close(out[0]);
} else if (!inherit_stdout) {
// Close stdout in the child process
close(STDOUT_FILENO);
}
if (perr) {
if (err[1] != STDERR_FILENO) {
dup2(err[1], STDERR_FILENO);
close(err[1]);
}
close(err[0]);
} else if (!inherit_stderr) {
// Close stderr in the child process
close(STDERR_FILENO);
}
close(internal[0]);
enum sc_process_result err;
if (fcntl(internal[1], F_SETFD, FD_CLOEXEC) == 0) {

View File

@ -26,7 +26,7 @@ bool
sc_file_is_regular(const char *path) {
wchar_t *wide_path = sc_str_to_wchars(path);
if (!wide_path) {
LOGC("Could not allocate wide char string");
LOG_OOM();
return false;
}

View File

@ -1,5 +1,7 @@
#include "util/process.h"
#include <processthreadsapi.h>
#include <assert.h>
#include "util/log.h"
@ -22,8 +24,16 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) {
}
enum sc_process_result
sc_process_execute_p(const char *const argv[], HANDLE *handle,
sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags,
HANDLE *pin, HANDLE *pout, HANDLE *perr) {
bool inherit_stdout = !pout && !(flags & SC_PROCESS_NO_STDOUT);
bool inherit_stderr = !perr && !(flags & SC_PROCESS_NO_STDERR);
// Add 1 per non-NULL pointer
unsigned handle_count = !!pin
+ (pout || inherit_stdout)
+ (perr || inherit_stderr);
enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC;
SECURITY_ATTRIBUTES sa;
@ -65,45 +75,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;
unsigned i = 0;
if (pin) {
si.hStdInput = stdin_read_handle;
si.StartupInfo.hStdInput = stdin_read_handle;
handles[i++] = si.StartupInfo.hStdInput;
}
if (pout) {
si.hStdOutput = stdout_write_handle;
if (pout || inherit_stdout) {
si.StartupInfo.hStdOutput = pout ? stdout_write_handle
: GetStdHandle(STD_OUTPUT_HANDLE);
handles[i++] = si.StartupInfo.hStdOutput;
}
if (perr) {
si.hStdError = stderr_write_handle;
if (perr || inherit_stderr) {
si.StartupInfo.hStdError = perr ? stderr_write_handle
: GetStdHandle(STD_ERROR_HANDLE);
handles[i++] = si.StartupInfo.hStdError;
}
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) {
LOG_OOM();
goto error_close_stderr;
}
ok = InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &size);
if (!ok) {
free(lpAttributeList);
goto error_close_stderr;
}
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;
LOG_OOM();
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;
LOG_OOM();
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;
// DETACHED_PROCESS to disable stdin, stdout and stderr
DWORD dwCreationFlags = handle_count > 0 ? EXTENDED_STARTUPINFO_PRESENT
: DETACHED_PROCESS;
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 +176,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 +231,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 +240,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

@ -31,7 +31,7 @@ sc_file_get_local_path(const char *name) {
size_t len = dirlen + namelen + 2; // +2: '/' and '\0'
char *file_path = malloc(len);
if (!file_path) {
LOGE("Could not alloc path");
LOG_OOM();
free(executable_path);
return NULL;
}

13
app/src/util/intmap.c Normal file
View File

@ -0,0 +1,13 @@
#include "intmap.h"
const struct sc_intmap_entry *
sc_intmap_find_entry(const struct sc_intmap_entry entries[], size_t len,
int key) {
for (size_t i = 0; i < len; ++i) {
const struct sc_intmap_entry *entry = &entries[i];
if (entry->key == key) {
return entry;
}
}
return NULL;
}

22
app/src/util/intmap.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef SC_ARRAYMAP_H
#define SC_ARRAYMAP_H
#include "common.h"
struct sc_intmap_entry {
int key;
int value;
};
const struct sc_intmap_entry *
sc_intmap_find_entry(const struct sc_intmap_entry entries[], size_t len,
int key);
/**
* MAP is expected to be a static array of sc_intmap_entry, so that
* ARRAY_LEN(MAP) can be computed statically.
*/
#define SC_INTMAP_FIND_ENTRY(MAP, KEY) \
sc_intmap_find_entry(MAP, ARRAY_LEN(MAP), KEY)
#endif

View File

@ -8,7 +8,7 @@ bool
sc_intr_init(struct sc_intr *intr) {
bool ok = sc_mutex_init(&intr->mutex);
if (!ok) {
LOGE("Could not init intr mutex");
LOG_OOM();
return false;
}

View File

@ -7,6 +7,9 @@
#include "options.h"
#define LOG_STR_IMPL_(x) # x
#define LOG_STR(x) LOG_STR_IMPL_(x)
#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
@ -14,6 +17,9 @@
#define LOGE(...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGC(...) SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOG_OOM() \
LOGC("OOM: %s:%d %s()", __FILE__, __LINE__, __func__)
void
sc_set_log_level(enum sc_log_level level);

View File

@ -1,25 +1,30 @@
#include "net.h"
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <SDL2/SDL_platform.h>
#include "log.h"
#ifdef __WINDOWS__
# include <ws2tcpip.h>
typedef int socklen_t;
typedef SOCKET sc_raw_socket;
# define SC_RAW_SOCKET_NONE INVALID_SOCKET
#else
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <unistd.h>
# include <fcntl.h>
# define SOCKET_ERROR -1
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
typedef struct in_addr IN_ADDR;
typedef int sc_raw_socket;
# define SC_RAW_SOCKET_NONE -1
#endif
bool
@ -51,6 +56,7 @@ wrap(sc_raw_socket sock) {
struct sc_socket_windows *socket = malloc(sizeof(*socket));
if (!socket) {
LOG_OOM();
closesocket(sock);
return SC_SOCKET_NONE;
}
@ -77,6 +83,35 @@ unwrap(sc_socket socket) {
#endif
}
static inline bool
sc_raw_socket_close(sc_raw_socket raw_sock) {
#ifndef _WIN32
return !close(raw_sock);
#else
return !closesocket(raw_sock);
#endif
}
#ifndef HAVE_SOCK_CLOEXEC
// If SOCK_CLOEXEC does not exist, the flag must be set manually once the
// socket is created
static bool
set_cloexec_flag(sc_raw_socket raw_sock) {
#ifndef _WIN32
if (fcntl(raw_sock, F_SETFD, FD_CLOEXEC) == -1) {
perror("fcntl F_SETFD");
return false;
}
#else
if (!SetHandleInformation((HANDLE) raw_sock, HANDLE_FLAG_INHERIT, 0)) {
LOGE("SetHandleInformation socket failed");
return false;
}
#endif
return true;
}
#endif
static void
net_perror(const char *s) {
#ifdef _WIN32
@ -95,7 +130,16 @@ net_perror(const char *s) {
sc_socket
net_socket(void) {
#ifdef HAVE_SOCK_CLOEXEC
sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
#else
sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM, 0);
if (raw_sock != SC_RAW_SOCKET_NONE && !set_cloexec_flag(raw_sock)) {
sc_raw_socket_close(raw_sock);
return SC_SOCKET_NONE;
}
#endif
sc_socket sock = wrap(raw_sock);
if (sock == SC_SOCKET_NONE) {
net_perror("socket");
@ -154,8 +198,18 @@ net_accept(sc_socket server_socket) {
SOCKADDR_IN csin;
socklen_t sinsize = sizeof(csin);
#ifdef HAVE_SOCK_CLOEXEC
sc_raw_socket raw_sock =
accept4(raw_server_socket, (SOCKADDR *) &csin, &sinsize, SOCK_CLOEXEC);
#else
sc_raw_socket raw_sock =
accept(raw_server_socket, (SOCKADDR *) &csin, &sinsize);
if (raw_sock != SC_RAW_SOCKET_NONE && !set_cloexec_flag(raw_sock)) {
sc_raw_socket_close(raw_sock);
return SC_SOCKET_NONE;
}
#endif
return wrap(raw_sock);
}
@ -209,7 +263,6 @@ net_interrupt(sc_socket socket) {
#endif
}
#include <errno.h>
bool
net_close(sc_socket socket) {
sc_raw_socket raw_sock = unwrap(socket);
@ -225,3 +278,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

@ -5,27 +5,8 @@
#include "log.h"
enum sc_process_result
sc_process_execute(const char *const argv[], sc_pid *pid) {
return sc_process_execute_p(argv, pid, NULL, NULL, NULL);
}
bool
sc_process_check_success(sc_pid pid, const char *name, bool close) {
if (pid == SC_PROCESS_NONE) {
LOGE("Could not execute \"%s\"", name);
return false;
}
sc_exit_code exit_code = sc_process_wait(pid, close);
if (exit_code) {
if (exit_code != SC_EXIT_CODE_NONE) {
LOGE("\"%s\" returned with value %" SC_PRIexitcode, name,
exit_code);
} else {
LOGE("\"%s\" exited unexpectedly", name);
}
return false;
}
return true;
sc_process_execute(const char *const argv[], sc_pid *pid, unsigned flags) {
return sc_process_execute_p(argv, pid, flags, NULL, NULL, NULL);
}
ssize_t

View File

@ -67,20 +67,32 @@ enum sc_process_result {
SC_PROCESS_ERROR_MISSING_BINARY,
};
#define SC_PROCESS_NO_STDOUT (1 << 0)
#define SC_PROCESS_NO_STDERR (1 << 1)
/**
* Execute the command and write the process id to `pid`
*
* The `flags` argument is a OR of any of:
* - SC_PROCESS_NO_STDOUT
* - SC_PROCESS_NO_STDERR
*
* It indicates if stdout and stderr must be inherited from the scrcpy process
* (if the process must output to the scrcpy console).
*/
enum sc_process_result
sc_process_execute(const char *const argv[], sc_pid *pid);
sc_process_execute(const char *const argv[], sc_pid *pid, unsigned flags);
/**
* Execute the command and write the process id to `pid`
*
* If not NULL, provide a pipe for stdin (`pin`), stdout (`pout`) and stderr
* (`perr`).
*
* The `flags` has the same semantics as in `sc_process_execute()`.
*/
enum sc_process_result
sc_process_execute_p(const char *const argv[], sc_pid *pid,
sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags,
sc_pipe *pin, sc_pipe *pout, sc_pipe *perr);
/**
@ -107,14 +119,6 @@ sc_process_wait(sc_pid pid, bool close);
void
sc_process_close(sc_pid pid);
/**
* Convenience function to wait for a successful process execution
*
* Automatically log process errors with the provided process name.
*/
bool
sc_process_check_success(sc_pid pid, const char *name, bool close);
/**
* Read from the pipe
*

View File

@ -1,15 +1,28 @@
#include "process_intr.h"
bool
sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid,
const char *name) {
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;
}
// Always pass close=false, interrupting would be racy otherwise
bool ret = sc_process_check_success(pid, name, 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

@ -6,8 +6,12 @@
#include "intr.h"
#include "process.h"
bool
sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid,
const char *name);
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

@ -5,13 +5,15 @@
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include "util/strbuf.h"
#ifdef _WIN32
# include <windows.h>
# include <tchar.h>
#endif
#include "log.h"
#include "strbuf.h"
size_t
sc_strncpy(char *dest, const char *src, size_t n) {
size_t i;
@ -51,6 +53,7 @@ sc_str_quote(const char *src) {
size_t len = strlen(src);
char *quoted = malloc(len + 3);
if (!quoted) {
LOG_OOM();
return NULL;
}
memcpy(&quoted[1], src, len);
@ -188,6 +191,7 @@ sc_str_to_wchars(const char *utf8) {
wchar_t *wide = malloc(len * sizeof(wchar_t));
if (!wide) {
LOG_OOM();
return NULL;
}
@ -204,6 +208,7 @@ sc_str_from_wchars(const wchar_t *ws) {
char *utf8 = malloc(len);
if (!utf8) {
LOG_OOM();
return NULL;
}
@ -291,3 +296,38 @@ 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;
}
ssize_t
sc_str_index_of_column(const char *s, unsigned col, const char *seps) {
size_t colidx = 0;
size_t idx = 0;
while (s[idx] != '\0' && colidx != col) {
size_t r = strcspn(&s[idx], seps);
assert(r);
idx += r;
if (s[idx] == '\0') {
// Not found
return -1;
}
size_t consecutive_seps = strspn(&s[idx], seps);
assert(consecutive_seps); // At least one
idx += consecutive_seps;
if (s[idx] != '\0') {
++colidx;
}
}
return col == colidx ? (ssize_t) idx : -1;
}

View File

@ -103,4 +103,35 @@ 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);
/**
* Find the start of a column in a string
*
* A string may represent several columns, separated by some "spaces"
* (separators). This function aims to find the start of the column number
* `col`.
*
* For example, to find the 4th column (column number 3):
*
* // here
* // v
* const char *s = "abc def ghi jk";
* ssize_t index = sc_str_index_of_column(s, 3, " ");
* assert(index == 16); // points to "jk"
*
* Returns -1 if no such column exists.
*/
ssize_t
sc_str_index_of_column(const char *s, unsigned col, const char *seps);
#endif

View File

@ -1,15 +1,17 @@
#include "strbuf.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "log.h"
bool
sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap) {
buf->s = malloc(init_cap + 1); // +1 for '\0'
if (!buf->s) {
LOG_OOM();
return false;
}
@ -25,6 +27,7 @@ sc_strbuf_reserve(struct sc_strbuf *buf, size_t len) {
char *s = realloc(buf->s, new_cap + 1); // +1 for '\0'
if (!s) {
// Leave the old buf->s
LOG_OOM();
return false;
}
buf->s = s;

View File

@ -10,6 +10,7 @@ sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
void *userdata) {
SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata);
if (!sdl_thread) {
LOG_OOM();
return false;
}
@ -26,6 +27,7 @@ bool
sc_mutex_init(sc_mutex *mutex) {
SDL_mutex *sdl_mutex = SDL_CreateMutex();
if (!sdl_mutex) {
LOG_OOM();
return false;
}
@ -94,6 +96,7 @@ bool
sc_cond_init(sc_cond *cond) {
SDL_cond *sdl_cond = SDL_CreateCond();
if (!sdl_cond) {
LOG_OOM();
return false;
}

View File

@ -1,5 +1,7 @@
#include "v4l2_sink.h"
#include <string.h>
#include "util/log.h"
#include "util/str.h"
@ -31,7 +33,7 @@ write_header(struct sc_v4l2_sink *vs, const AVPacket *packet) {
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) {
LOGC("Could not allocate extradata");
LOG_OOM();
return false;
}
@ -161,25 +163,21 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
bool ok = sc_video_buffer_init(&vs->vb, vs->buffering_time, &cbs, vs);
if (!ok) {
LOGE("Could not initialize video buffer");
return false;
}
ok = sc_video_buffer_start(&vs->vb);
if (!ok) {
LOGE("Could not start video buffer");
goto error_video_buffer_destroy;
}
ok = sc_mutex_init(&vs->mutex);
if (!ok) {
LOGC("Could not create mutex");
goto error_video_buffer_stop_and_join;
}
ok = sc_cond_init(&vs->cond);
if (!ok) {
LOGC("Could not create cond");
goto error_mutex_destroy;
}
@ -201,7 +199,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
vs->format_ctx = avformat_alloc_context();
if (!vs->format_ctx) {
LOGE("Could not allocate v4l2 output context");
LOG_OOM();
return false;
}
@ -213,9 +211,8 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
#ifdef SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL
vs->format_ctx->url = strdup(vs->device_name);
if (!vs->format_ctx->url) {
LOGE("Could not strdup v4l2 device name");
LOG_OOM();
goto error_avformat_free_context;
return false;
}
#else
strncpy(vs->format_ctx->filename, vs->device_name,
@ -224,9 +221,8 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
AVStream *ostream = avformat_new_stream(vs->format_ctx, encoder);
if (!ostream) {
LOGE("Could not allocate new v4l2 stream");
LOG_OOM();
goto error_avformat_free_context;
return false;
}
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
@ -244,7 +240,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
vs->encoder_ctx = avcodec_alloc_context3(encoder);
if (!vs->encoder_ctx) {
LOGC("Could not allocate codec context for v4l2");
LOG_OOM();
goto error_avio_close;
}
@ -261,13 +257,13 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
vs->frame = av_frame_alloc();
if (!vs->frame) {
LOGE("Could not create v4l2 frame");
LOG_OOM();
goto error_avcodec_close;
}
vs->packet = av_packet_alloc();
if (!vs->packet) {
LOGE("Could not allocate packet");
LOG_OOM();
goto error_av_frame_free;
}

View File

@ -14,11 +14,13 @@ static struct sc_video_buffer_frame *
sc_video_buffer_frame_new(const AVFrame *frame) {
struct sc_video_buffer_frame *vb_frame = malloc(sizeof(*vb_frame));
if (!vb_frame) {
LOG_OOM();
return NULL;
}
vb_frame->frame = av_frame_alloc();
if (!vb_frame->frame) {
LOG_OOM();
free(vb_frame);
return NULL;
}
@ -132,14 +134,12 @@ sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time,
if (buffering_time) {
ok = sc_mutex_init(&vb->b.mutex);
if (!ok) {
LOGC("Could not create mutex");
sc_frame_buffer_destroy(&vb->fb);
return false;
}
ok = sc_cond_init(&vb->b.queue_cond);
if (!ok) {
LOGC("Could not create cond");
sc_mutex_destroy(&vb->b.mutex);
sc_frame_buffer_destroy(&vb->fb);
return false;
@ -147,7 +147,6 @@ sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time,
ok = sc_cond_init(&vb->b.wait_cond);
if (!ok) {
LOGC("Could not create wait cond");
sc_cond_destroy(&vb->b.queue_cond);
sc_mutex_destroy(&vb->b.mutex);
sc_frame_buffer_destroy(&vb->fb);
@ -234,7 +233,7 @@ sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) {
struct sc_video_buffer_frame *vb_frame = sc_video_buffer_frame_new(frame);
if (!vb_frame) {
sc_mutex_unlock(&vb->b.mutex);
LOGE("Could not allocate frame");
LOG_OOM();
return false;
}

View File

@ -89,7 +89,7 @@ static void test_options(void) {
assert(!strcmp(opts->serial, "0123456789abcdef"));
assert(opts->show_touches);
assert(opts->turn_screen_off);
assert(opts->prefer_text);
assert(opts->key_inject_mode == SC_KEY_INJECT_MODE_TEXT);
assert(!strcmp(opts->window_title, "my device"));
assert(opts->window_x == 100);
assert(opts->window_y == -1);

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

@ -3,6 +3,7 @@
#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "util/str.h"
@ -337,6 +338,50 @@ 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));
}
static void test_index_of_column(void) {
assert(sc_str_index_of_column("a bc d", 0, " ") == 0);
assert(sc_str_index_of_column("a bc d", 1, " ") == 2);
assert(sc_str_index_of_column("a bc d", 2, " ") == 6);
assert(sc_str_index_of_column("a bc d", 3, " ") == -1);
assert(sc_str_index_of_column("a ", 0, " ") == 0);
assert(sc_str_index_of_column("a ", 1, " ") == -1);
assert(sc_str_index_of_column("", 0, " ") == 0);
assert(sc_str_index_of_column("", 1, " ") == -1);
assert(sc_str_index_of_column("a \t \t bc \t d\t", 0, " \t") == 0);
assert(sc_str_index_of_column("a \t \t bc \t d\t", 1, " \t") == 8);
assert(sc_str_index_of_column("a \t \t bc \t d\t", 2, " \t") == 15);
assert(sc_str_index_of_column("a \t \t bc \t d\t", 3, " \t") == -1);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
@ -356,5 +401,7 @@ int main(int argc, char *argv[]) {
test_parse_integer_with_suffix();
test_strlist_contains();
test_wrap_lines();
test_truncate();
test_index_of_column();
return 0;
}

View File

@ -2,6 +2,7 @@
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "util/strbuf.h"

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

@ -21,7 +21,7 @@ public class CodecOption {
}
public static List<CodecOption> parse(String codecOptions) {
if ("-".equals(codecOptions)) {
if (codecOptions.isEmpty()) {
return null;
}

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);
@ -65,7 +65,7 @@ public final class Device {
int displayInfoFlags = displayInfo.getFlags();
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockVideoOrientation());
layerStack = displayInfo.getLayerStack();
SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
@ -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

@ -2,22 +2,25 @@ package com.genymobile.scrcpy;
import android.graphics.Rect;
import java.util.List;
public class Options {
private Ln.Level logLevel;
private Ln.Level logLevel = Ln.Level.DEBUG;
private int maxSize;
private int bitRate;
private int bitRate = 8000000;
private int maxFps;
private int lockedVideoOrientation;
private int lockVideoOrientation = -1;
private boolean tunnelForward;
private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly
private boolean control;
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
private boolean control = true;
private int displayId;
private boolean showTouches;
private boolean stayAwake;
private String codecOptions;
private List<CodecOption> codecOptions;
private String encoderName;
private boolean powerOffScreenOnClose;
private boolean clipboardAutosync = true;
public Ln.Level getLogLevel() {
return logLevel;
@ -51,12 +54,12 @@ public class Options {
this.maxFps = maxFps;
}
public int getLockedVideoOrientation() {
return lockedVideoOrientation;
public int getLockVideoOrientation() {
return lockVideoOrientation;
}
public void setLockedVideoOrientation(int lockedVideoOrientation) {
this.lockedVideoOrientation = lockedVideoOrientation;
public void setLockVideoOrientation(int lockVideoOrientation) {
this.lockVideoOrientation = lockVideoOrientation;
}
public boolean isTunnelForward() {
@ -115,11 +118,11 @@ public class Options {
this.stayAwake = stayAwake;
}
public String getCodecOptions() {
public List<CodecOption> getCodecOptions() {
return codecOptions;
}
public void setCodecOptions(String codecOptions) {
public void setCodecOptions(List<CodecOption> codecOptions) {
this.codecOptions = codecOptions;
}
@ -138,4 +141,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 = 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,64 +160,93 @@ public final class Server {
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
final int expectedParameters = 16;
if (args.length != expectedParameters) {
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
}
Options options = new Options();
Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH));
options.setLogLevel(level);
int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8
options.setMaxSize(maxSize);
int bitRate = Integer.parseInt(args[3]);
options.setBitRate(bitRate);
int maxFps = Integer.parseInt(args[4]);
options.setMaxFps(maxFps);
int lockedVideoOrientation = Integer.parseInt(args[5]);
options.setLockedVideoOrientation(lockedVideoOrientation);
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[6]);
options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[7]);
options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[8]);
options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[9]);
options.setControl(control);
int displayId = Integer.parseInt(args[10]);
options.setDisplayId(displayId);
boolean showTouches = Boolean.parseBoolean(args[11]);
options.setShowTouches(showTouches);
boolean stayAwake = Boolean.parseBoolean(args[12]);
options.setStayAwake(stayAwake);
String codecOptions = args[13];
options.setCodecOptions(codecOptions);
String encoderName = "-".equals(args[14]) ? null : args[14];
options.setEncoderName(encoderName);
boolean powerOffScreenOnClose = Boolean.parseBoolean(args[15]);
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
for (int i = 1; i < args.length; ++i) {
String arg = args[i];
int equalIndex = arg.indexOf('=');
if (equalIndex == -1) {
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
}
String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1);
switch (key) {
case "log_level":
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
options.setLogLevel(level);
break;
case "max_size":
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
options.setMaxSize(maxSize);
break;
case "bit_rate":
int bitRate = Integer.parseInt(value);
options.setBitRate(bitRate);
break;
case "max_fps":
int maxFps = Integer.parseInt(value);
options.setMaxFps(maxFps);
break;
case "lock_video_orientation":
int lockVideoOrientation = Integer.parseInt(value);
options.setLockVideoOrientation(lockVideoOrientation);
break;
case "tunnel_forward":
boolean tunnelForward = Boolean.parseBoolean(value);
options.setTunnelForward(tunnelForward);
break;
case "crop":
Rect crop = parseCrop(value);
options.setCrop(crop);
break;
case "send_frame_meta":
boolean sendFrameMeta = Boolean.parseBoolean(value);
options.setSendFrameMeta(sendFrameMeta);
break;
case "control":
boolean control = Boolean.parseBoolean(value);
options.setControl(control);
break;
case "display_id":
int displayId = Integer.parseInt(value);
options.setDisplayId(displayId);
break;
case "show_touches":
boolean showTouches = Boolean.parseBoolean(value);
options.setShowTouches(showTouches);
break;
case "stay_awake":
boolean stayAwake = Boolean.parseBoolean(value);
options.setStayAwake(stayAwake);
break;
case "codec_options":
List<CodecOption> codecOptions = CodecOption.parse(value);
options.setCodecOptions(codecOptions);
break;
case "encoder_name":
if (!value.isEmpty()) {
options.setEncoderName(value);
}
break;
case "power_off_on_close":
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
break;
case "clipboard_autosync":
boolean clipboardAutosync = Boolean.parseBoolean(value);
options.setClipboardAutosync(clipboardAutosync);
break;
default:
Ln.w("Unknown server option: " + key);
break;
}
}
return options;
}
private static Rect parseCrop(String crop) {
if ("-".equals(crop)) {
if (crop.isEmpty()) {
return null;
}
// input format: "width:height:x:y"

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);
}
}