Compare commits

...

76 Commits

Author SHA1 Message Date
b0a4e6df25 Retry on recoverable MediaCodec errors
Refs <https://developer.android.com/reference/android/media/MediaCodec#error-handling>
Fixes #3693 <https://github.com/Genymobile/scrcpy/issues/3693>
2023-01-27 23:15:44 +01:00
545a8a8f32 Extract downsize-retry handling
Move the code to downscale and retry on error out of the catch-block.

Refs 26b4104844
2023-01-27 23:15:03 +01:00
a9b2697f3e Move local variables declarations
This makes it clear that these local variables are only passed to
setDisplaySurface().
2023-01-27 22:43:16 +01:00
b53d2c66e0 Remove useless setSize() method
Inline its content. It makes the logic of internalStreamScreen() more
readable (it reduces the number of indirections).
2023-01-27 22:39:28 +01:00
6cccf3ab2a Remove useless configure() method
Inline its single call.
2023-01-27 22:38:37 +01:00
52f85fd6f1 Keep the same MediaCodec instance across sessions
Calling codec.reset() is sufficient.
2023-01-27 22:31:09 +01:00
91c69ad95c Remove useless destroyDisplay() method
The method made exactly one simple call. Just make the call directly.
2023-01-27 22:28:11 +01:00
75d7c01a0c Keep the same display binder across sessions
Do not destroy/recreate the display when starting a new encoding session
(on device rotation for example).
2023-01-27 22:26:01 +01:00
74d32e612d Terminate loop explicitly on interrupted
Make explicit that the loop terminates when the current thread is
interrupted.
2023-01-27 22:20:35 +01:00
bdba554118 Use Java lambdas where possible 2023-01-27 22:16:36 +01:00
234ad7ee78 Support Java lambdas in build_without_gradle.sh
Building Java source code using lambdas requires core-lambda-stubs.jar.

Refs #3657 <https://github.com/Genymobile/scrcpy/issues/3657>
2023-01-27 22:08:25 +01:00
8cbbcc939f Add missing final modifiers 2023-01-27 22:08:17 +01:00
b22810b17c Use try-with-resources
Replace an explicit try-finally by a try-with-resources block.
2023-01-27 21:59:26 +01:00
4315be1648 Use random name for device socket
For the initial connection between the device and the computer, an adb
tunnel is established (with "adb reverse" or "adb forward").

The device-side of the tunnel is a local socket having the hard-coded
name "scrcpy". This may cause issues when several scrcpy instances are
started in a few seconds for the same device, since they will try to
bind the same name.

To avoid conflicts, make the client generate a random UID, and append
this UID to the local socket name ("scrcpy_01234567").
2023-01-27 21:51:59 +01:00
74e3f8b253 Add random util
Add a user-friendly tool to generate random numbers.
2023-01-27 19:26:19 +01:00
059ec45f82 Add jrand48()/nrand48() compat functions
These functions are not available on all platforms.
2023-01-26 18:11:23 +01:00
e6cd42355b Use separate gen dir to build without gradle
The generated source files were written to the classes dir. Use a
separate gen dir instead.
2023-01-26 10:44:31 +01:00
bf8696d02e Avoid unnecessary copy on config packets demuxing
Use av_packet_ref() to reference the packet without copy.

This also simplifies the logic, by making the "offset" variable and the
memcpy() call local to the if-block.
2023-01-02 16:18:23 +01:00
d8c2fe6ef2 Revert "Remove continuous resizing workaround for Windows"
This reverts commit 18082f6069.

I can't reproduce, but it seems the workaround improves the behavior on
some Windows versions.

Fixes #3640 <https://github.com/Genymobile/scrcpy/issues/3640>
Refs #3458 <https://github.com/Genymobile/scrcpy/issues/3458>
2022-12-26 12:42:59 +01:00
54c7baceac Use "meson setup" from install_release.sh
Refs 64821466a1
2022-12-22 13:07:07 +01:00
4c43784fd1 Update links to v1.25 2022-12-22 12:44:01 +01:00
fe21158c20 Bump version to 1.25 2022-12-22 12:33:29 +01:00
8e0c899218 Upgrade FFmpeg (5.1.2) for Windows 64-bit
Use the latest version of FFmpeg in Windows 64-bit releases.
2022-12-22 12:33:08 +01:00
725a922271 Upgrade SDL (2.26.1) for Windows
Include the latest version of SDL in Windows releases.
2022-12-22 12:29:08 +01:00
b5773a6fe8 Upgrade platform-tools (33.0.3) for Windows
Include the latest version of adb in Windows releases.
2022-12-22 12:29:08 +01:00
67fb457dcb Merge branch 'master' into dev 2022-12-22 12:29:00 +01:00
c7b1d0ea9a Force mouse source when --forward-all-clicks
Right click and middle click require the source device to be a mouse,
not a touchscreen. Therefore, the source device was changed only when a
button other than the primary button was pressed (see
adc547fa6e).

However, this led to inconsistencies between the ACTION_DOWN when a
secondary button is pressed (with a mouse as source device) and the
matching ACTION_UP when the secondary button is released (with a
touchscreen as source device, because then there is no button pressed).

To avoid the problem in all cases, force a mouse as source device when
--forward-all-clicks is set.

Concretely, for mouse events in --forward-all-clicks mode:
 - device source is set to InputDevice.SOURCE_MOUSE;
 - motion event toolType is set to MotionEvent.TOOL_TYPE_MOUSE;

Otherwise (when --forward-all-clicks is unset, or for real touch
events), finger events are injected:
 - device source is set to InputDevice.SOURCE_TOUCHSCREEN;
 - motion event toolType is set to MotionEvent.TOOL_TYPE_FINGER.

Fixes #3568 <https://github.com/Genymobile/scrcpy/issues/3568>
PR #3579 <https://github.com/Genymobile/scrcpy/pull/3579>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-12-22 11:26:26 +01:00
18082f6069 Remove continuous resizing workaround for Windows
It turns out that the workaround only worked for MacOS.

Refs #3458 <https://github.com/Genymobile/scrcpy/issues/3458>
Refs SDL/#1059 <https://github.com/libsdl-org/SDL/issues/1059>
2022-12-21 22:06:43 +01:00
8b38b11875 Add parent directory in release zipfile
This avoids to mistakenly extract all the files in the current
directory.
2022-12-21 13:31:18 +01:00
64821466a1 Use "meson setup"
This fixes the following warning:

> WARNING: Running the setup command as `meson [options]` instead of
> `meson setup [options]` is ambiguous and deprecated.
2022-12-21 13:29:27 +01:00
82cb8ab870 Adapt ClipboardManager for Android 13
A new "attributionTag" parameter has been added to the methods
getPrimaryClip(), setPrimaryClip() and addPrimaryClipChangedListener()
of IClipboard.aidl.

Refs <0e3e509b3b%5E%21/>

Fixes #3497 <https://github.com/Genymobile/scrcpy/issues/3497>
2022-12-21 13:28:22 +01:00
b51841e85d Upgrade junit to 4.13.2 2022-12-21 13:28:22 +01:00
bd1deffa70 Use current adb port (if any) for --tcpip
If the current adb port is not 5555 (typically 0 because it is not in
TCP/IP mode), --tcpip automatically executes (among other commands):

    adb tcpip 5555

In case adb was already listening on another port, this command forced
to listen on 5555, and the connection should still succeed.

But this reconfiguration might be inconvenient for the user. If adb is
already in TCP/IP mode, use the current enabled port without
reconfiguration.

Fixes #3591 <https://github.com/Genymobile/scrcpy/issues/3591>
2022-12-02 19:09:53 +01:00
6469b55861 Fix CommandParserTest code style
Make checkstyle happy.
2022-11-24 09:27:10 +01:00
c00a9ead5e Always use --key=value in README
Mandatory arguments may be passed in either of these two forms:
 1. --key value
 2. --key=value

Optional argument may only be passed in the second form.

For consistency, always document using --key=value.

Refs f76fe2c0d4
2022-11-17 09:27:05 +01:00
597703b62e Fix DisplayInfo parsing for Android Q
The DisplayInfo dump format has slightly changed in AOSP:
<1039ea50f3>

PR #3573 <https://github.com/Genymobile/scrcpy/pull/3573>
Ref #3416 <https://github.com/Genymobile/scrcpy/pull/3416>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-11-12 18:15:45 +01:00
48bb6f2ea8 Support wchar_t in argv for Windows
PR #3547 <https://github.com/Genymobile/scrcpy/pull/3547>
Fixes #2932 <https://github.com/Genymobile/scrcpy/issues/2932>

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-10-23 23:45:00 +02:00
d71587e39b Avoid string concatenation in crossfiles
This feature is not supported on older meson versions:

    ERROR: Malformed value in cross file variable prebuilt_libusb.

Refs <https://github.com/mesonbuild/meson/issues/3878>
PR #3546 <https://github.com/Genymobile/scrcpy/pull/3546>

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-10-23 12:31:44 +02:00
b62424a98a Build log.c for test_cli
On Windows, sc_log_windows_error() is called from net.c, so log.c must
also be compiled.

Fixes #3542 <https://github.com/Genymobile/scrcpy/issues/3542>
2022-10-19 15:17:43 +02:00
ffc7b91693 Add missing include <string.h> for strlen() 2022-10-19 15:14:56 +02:00
cb46e4a64a Add missing include <string.h> for memmove() 2022-10-19 15:13:55 +02:00
16e2c1ce26 Add -s auto-completion for zsh
Fixes #3522 <https://github.com/Genymobile/scrcpy/pull/3522>
PR #3523 <https://github.com/Genymobile/scrcpy/pull/3523>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-10-12 13:24:43 +02:00
1bfbadef96 Add -s auto-completion for bash
Fixes #3522 <https://github.com/Genymobile/scrcpy/pull/3522>
PR #3523 <https://github.com/Genymobile/scrcpy/pull/3523>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-10-12 13:24:43 +02:00
40644994e8 Make ServiceManager and Settings methods static
There were exactly one instance of ServiceManager and Settings, stored
in Device.

Since a Device instance is not created by the CleanUp executable, it was
not straightforward to call wrapper methods on cleanup.

Remove this artificial restriction and expose them publicly via static
methods (this is equivalent to expose a singleton, but less verbose).
2022-10-02 17:57:35 +02:00
7505f7117e Fix typo in logs 2022-09-27 14:12:37 +02:00
949b64dff2 Add fallback to get DisplayInfo
PR #3416 <https://github.com/Genymobile/scrcpy/pull/3416>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-09-25 16:22:51 +02:00
00e9e69c2a Use /dev/null instead of closing fds
Some adb commands do not like when stdin, stdout or stderr are closed
(they hang forever). Open /dev/null for each.
2022-09-25 15:42:33 +02:00
4a5cdcd390 Extract $BUILD_TOOLS_DIR
In the script to build without gradle, the build-tools full path is used
at several places. Use a separate variable for readability.
2022-09-25 14:26:07 +02:00
e5e210506f Add scrcpy-console.desktop
Add a launcher which opens a terminal, and keep it open in case of
errors (so that the user has time to read error messages).

The behavior is the same as scrcpy-console.bat on Windows.

PR #3351 <https://github.com/Genymobile/scrcpy/pull/3351>
2022-09-09 19:06:29 +02:00
a2a22f497f Use shell environment to execute launcher
Make Exec= compatible with $PATH configured in .bashrc or .zshrc…

PR #3351 <https://github.com/Genymobile/scrcpy/pull/3351>
Refs #296 <https://github.com/Genymobile/scrcpy/pull/296#discussion_r224987002>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-09-09 19:06:29 +02:00
51a1762cbd Add desktop entry file for Linux app launchers
Refs <https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html>

PR #3351 <https://github.com/Genymobile/scrcpy/pull/3351>
Replaces PR #296 <https://github.com/Genymobile/scrcpy/pull/296>
Fixes #295 <https://github.com/Genymobile/scrcpy/issues/295>
Fixes #748 <https://github.com/Genymobile/scrcpy/issues/748>
Fixes #1636 <https://github.com/Genymobile/scrcpy/issues/1636>

Co-authored-by: Chih-Hsuan Yen <yan12125@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-09-09 19:06:29 +02:00
c1ec1d1023 Replace hardcoded 'share/' by datadir variable
Meson defines a variable for the data directory.

PR #3351 <https://github.com/Genymobile/scrcpy/pull/3351>
2022-09-09 19:06:29 +02:00
0a0a446ea6 Upgrade Android SDK to 33 2022-09-02 14:42:37 +02:00
fccfc43b9e Upgrade gradle build tools to 7.2.2
Plugin version 7.2.2.
Gradle version 7.3.3.

Refs: <https://developer.android.com/studio/releases/gradle-plugin#updating-gradle>
2022-09-02 14:40:16 +02:00
121bb71dfe Move from jcenter() to mavenCentral()
Refs <https://developer.android.com/studio/build/jcenter-migration>
Refs <https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/>
2022-09-02 14:31:15 +02:00
57056d078d Use precise scrolling values
Since SDL 2.0.18, the amount scrolled horizontally or vertically is
exposed as a float (between 0 and 1). Forward a precise value to the
Android device when possible.

Refs <https://wiki.libsdl.org/SDL_MouseWheelEvent>
Fixes #3363 <https://github.com/Genymobile/scrcpy/issues/3363>
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-08-28 15:23:08 +02:00
1f138aef41 Add conversion from float to fixed-point i16
To encode float values between -1 and 1.

PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
1ab6c19486 Add unit test for float encoding
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
fd3483c837 Extract conversion from float to u16 fixed-point
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
041cdf6cf5 Rename buffer_util.h to binary.h
It will allow to expose more binary util functions not related to
buffers.

PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
136ab8c199 Add unit test for float decoding
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
3848ce86f1 Extract conversion from u16 fixed-point to float
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
5b8e9aa0e9 Move toUnsigned() to a Binary util class
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
3a66b5fd01 Remove deprecated meson.source_root()
This method is deprecated since Meson 0.56.0:
<https://mesonbuild.com/Release-notes-for-0-56-0.html#mesonbuild_root-and-mesonsource_root-are-deprecated>

We could replace it with meson.project_source_root(), but this would
make Meson 0.56 or above mandatory. Since the path in always computed
from the server/ directory, just add '..' to reference the root project
directory.

Refs c456e38264
2022-08-28 15:16:31 +02:00
9c1722f428 Use DisplayManagerGlobal instance
Use the client instance to communicate with the DisplayManager server.

Fixes #3446 <https://github.com/Genymobile/scrcpy/issues/3446>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-08-19 18:03:38 +02:00
d19606eb0c Rename net_listen() parameter
For consistency with net_accept(), which necessarily uses a server
socket, name the net_listen() parameter "server_socket".
2022-08-17 16:40:45 +02:00
d23b3e88a4 Replace '%g' by '%f' as printf format
For some reason, '%g' does not work correctly with MinGW.

Refs #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
PR #3399 <https://github.com/Genymobile/scrcpy/pull/3399>
2022-08-03 23:25:09 +02:00
a47848f304 Detect Windows using _WIN32 in network util
For consistency, always use _WIN32 instead of a mix of __WINDOWS__ and
_WIN32.
2022-07-27 14:54:27 +02:00
db8c1ce8e1 Fix protocol documentation in comments
Flags were in the correct order in the schema, but their description
were reversed.
2022-07-20 11:41:04 +02:00
4aeb78ece2 Add missing allocation failure check 2022-07-19 12:17:02 +02:00
396e4bd925 Add missing LOG_OOM() on malloc failure 2022-07-19 12:15:06 +02:00
7f2f5950f2 Remove useless dependencies reference
There is no libs/ directory with local jar files.
2022-06-20 21:23:05 +02:00
af4b7855e1 Remove unused stream.h
The file was not removed by 7dec225ceb.
2022-06-09 15:02:42 +02:00
b1d8c72780 Rename function to simplify
For consistency with sc_adb_parse_device(), do not include "from_output"
in the function name.
2022-06-09 15:02:42 +02:00
55e65fa270 Add missing return 0 in tests 2022-06-09 15:02:42 +02:00
69fb5f6ee1 Fix function declarations
Add missing void in function parameters list.
2022-06-09 15:02:42 +02:00
81 changed files with 1343 additions and 608 deletions

View File

@ -260,7 +260,7 @@ set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk
Then, build: Then, build:
```bash ```bash
meson x --buildtype=release --strip -Db_lto=true meson setup x --buildtype=release --strip -Db_lto=true
ninja -Cx # DO NOT RUN AS ROOT ninja -Cx # DO NOT RUN AS ROOT
``` ```
@ -272,16 +272,16 @@ install` must be run as root)._
#### Option 2: Use prebuilt server #### Option 2: Use prebuilt server
- [`scrcpy-server-v1.24`][direct-scrcpy-server] - [`scrcpy-server-v1.25`][direct-scrcpy-server]
<sub>SHA-256: `ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056`</sub> <sub>SHA-256: `ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:
```bash ```bash
meson x --buildtype=release --strip -Db_lto=true \ meson setup x --buildtype=release --strip -Db_lto=true \
-Dprebuilt_server=/path/to/scrcpy-server -Dprebuilt_server=/path/to/scrcpy-server
ninja -Cx # DO NOT RUN AS ROOT ninja -Cx # DO NOT RUN AS ROOT
``` ```

View File

@ -277,7 +277,7 @@ The server is pushed to the device by the client on startup.
To debug it, enable the server debugger during configuration: To debug it, enable the server debugger during configuration:
```bash ```bash
meson x -Dserver_debugger=true meson setup x -Dserver_debugger=true
# or, if x is already configured # or, if x is already configured
meson configure x -Dserver_debugger=true meson configure x -Dserver_debugger=true
``` ```
@ -286,7 +286,7 @@ If your device runs Android 8 or below, set the `server_debugger_method` to
`old` in addition: `old` in addition:
```bash ```bash
meson x -Dserver_debugger=true -Dserver_debugger_method=old meson setup x -Dserver_debugger=true -Dserver_debugger_method=old
# or, if x is already configured # or, if x is already configured
meson configure x -Dserver_debugger=true -Dserver_debugger_method=old meson configure x -Dserver_debugger=true -Dserver_debugger_method=old
``` ```

View File

@ -1,4 +1,4 @@
# scrcpy (v1.24) # scrcpy (v1.25)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" /> <img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -106,10 +106,10 @@ process][BUILD_simple]).
For Windows, a prebuilt archive with all the dependencies (including `adb`) is For Windows, a prebuilt archive with all the dependencies (including `adb`) is
available: available:
- [`scrcpy-win64-v1.24.zip`][direct-win64] - [`scrcpy-win64-v1.25.zip`][direct-win64]
<sub>SHA-256: `6ccb64cba0a3e75715e85a188daeb4f306a1985f8ce123eba92ba74fc9b27367`</sub> <sub>SHA-256: `db65125e9c65acd00359efb7cea9c05f63cc7ccd5833000cd243cc92f5053028`</sub>
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-win64-v1.24.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-win64-v1.25.zip
It is also available in [Chocolatey]: It is also available in [Chocolatey]:
@ -186,7 +186,7 @@ increase performance.
To limit both the width and height to some value (e.g. 1024): To limit both the width and height to some value (e.g. 1024):
```bash ```bash
scrcpy --max-size 1024 scrcpy --max-size=1024
scrcpy -m 1024 # short version scrcpy -m 1024 # short version
``` ```
@ -199,7 +199,7 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps): The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
```bash ```bash
scrcpy --bit-rate 2M scrcpy --bit-rate=2M
scrcpy -b 2M # short version scrcpy -b 2M # short version
``` ```
@ -208,7 +208,7 @@ scrcpy -b 2M # short version
The capture frame rate can be limited: The capture frame rate can be limited:
```bash ```bash
scrcpy --max-fps 15 scrcpy --max-fps=15
``` ```
This is officially supported since Android 10, but may work on earlier versions. This is officially supported since Android 10, but may work on earlier versions.
@ -229,7 +229,7 @@ The device screen may be cropped to mirror only part of the screen.
This is useful, for example, to mirror only one eye of the Oculus Go: This is useful, for example, to mirror only one eye of the Oculus Go:
```bash ```bash
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0)
``` ```
If `--max-size` is also specified, resizing is applied after cropping. If `--max-size` is also specified, resizing is applied after cropping.
@ -258,14 +258,14 @@ Some devices have more than one encoder, and some of them may cause issues or
crash. It is possible to select a different encoder: crash. It is possible to select a different encoder:
```bash ```bash
scrcpy --encoder OMX.qcom.video.encoder.avc scrcpy --encoder=OMX.qcom.video.encoder.avc
``` ```
To list the available encoders, you can pass an invalid encoder name; the To list the available encoders, you can pass an invalid encoder name; the
error will give the available encoders: error will give the available encoders:
```bash ```bash
scrcpy --encoder _ scrcpy --encoder=_
``` ```
### Capture ### Capture
@ -275,14 +275,14 @@ scrcpy --encoder _
It is possible to record the screen while mirroring: It is possible to record the screen while mirroring:
```bash ```bash
scrcpy --record file.mp4 scrcpy --record=file.mp4
scrcpy -r file.mkv scrcpy -r file.mkv
``` ```
To disable mirroring while recording: To disable mirroring while recording:
```bash ```bash
scrcpy --no-display --record file.mp4 scrcpy --no-display --record=file.mp4
scrcpy -Nr file.mkv scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C # interrupt recording with Ctrl+C
``` ```
@ -395,8 +395,8 @@ address), connect the device over USB, then run:
scrcpy --tcpip # without arguments scrcpy --tcpip # without arguments
``` ```
It will automatically find the device IP address, enable TCP/IP mode, then It will automatically find the device IP address and adb port, enable TCP/IP
connect to the device before starting. mode if necessary, then connect to the device before starting.
##### Manual ##### Manual
@ -431,7 +431,7 @@ none found, try running `adb disconnect`, and then run those two commands again.
It may be useful to decrease the bit-rate and the resolution: It may be useful to decrease the bit-rate and the resolution:
```bash ```bash
scrcpy --bit-rate 2M --max-size 800 scrcpy --bit-rate=2M --max-size=800
scrcpy -b2M -m800 # short version scrcpy -b2M -m800 # short version
``` ```
@ -443,7 +443,7 @@ scrcpy -b2M -m800 # short version
If several devices are listed in `adb devices`, you can specify the _serial_: If several devices are listed in `adb devices`, you can specify the _serial_:
```bash ```bash
scrcpy --serial 0123456789abcdef scrcpy --serial=0123456789abcdef
scrcpy -s 0123456789abcdef # short version scrcpy -s 0123456789abcdef # short version
``` ```
@ -453,7 +453,7 @@ The serial may also be provided via the environment variable `ANDROID_SERIAL`
If the device is connected over TCP/IP: If the device is connected over TCP/IP:
```bash ```bash
scrcpy --serial 192.168.0.1:5555 scrcpy --serial=192.168.0.1:5555
scrcpy -s 192.168.0.1:5555 # short version scrcpy -s 192.168.0.1:5555 # short version
``` ```
@ -606,7 +606,7 @@ scrcpy --force-adb-forward
Like for wireless connections, it may be useful to reduce quality: Like for wireless connections, it may be useful to reduce quality:
``` ```
scrcpy -b2M -m800 --max-fps 15 scrcpy -b2M -m800 --max-fps=15
``` ```
### Window configuration ### Window configuration
@ -616,7 +616,7 @@ scrcpy -b2M -m800 --max-fps 15
By default, the window title is the device model. It can be changed: By default, the window title is the device model. It can be changed:
```bash ```bash
scrcpy --window-title 'My device' scrcpy --window-title='My device'
``` ```
#### Position and size #### Position and size
@ -624,7 +624,7 @@ scrcpy --window-title 'My device'
The initial window position and size may be specified: The initial window position and size may be specified:
```bash ```bash
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 scrcpy --window-x=100 --window-y=100 --window-width=800 --window-height=600
``` ```
#### Borderless #### Borderless
@ -659,7 +659,7 @@ Fullscreen can then be toggled dynamically with <kbd>MOD</kbd>+<kbd>f</kbd>.
The window may be rotated: The window may be rotated:
```bash ```bash
scrcpy --rotation 1 scrcpy --rotation=1
``` ```
Possible values: Possible values:
@ -701,7 +701,7 @@ If several displays are available, it is possible to select the display to
mirror: mirror:
```bash ```bash
scrcpy --display 1 scrcpy --display=1
``` ```
The list of display ids can be retrieved by: The list of display ids can be retrieved by:

View File

@ -93,6 +93,11 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'verbose debug info warn error' -- "$cur")) COMPREPLY=($(compgen -W 'verbose debug info warn error' -- "$cur"))
return return
;; ;;
-s|--serial)
# Use 'adb devices' to list serial numbers
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
return
;;
-b|--bitrate \ -b|--bitrate \
|--codec-options \ |--codec-options \
|--crop \ |--crop \
@ -103,7 +108,6 @@ _scrcpy() {
|-m|--max-size \ |-m|--max-size \
|-p|--port \ |-p|--port \
|--push-target \ |--push-target \
|-s|--serial \
|--tunnel-host \ |--tunnel-host \
|--tunnel-port \ |--tunnel-port \
|--v4l2-buffer \ |--v4l2-buffer \

View File

@ -0,0 +1,13 @@
[Desktop Entry]
Name=scrcpy (console)
GenericName=Android Remote Control
Comment=Display and control your Android device
# For some users, the PATH or ADB environment variables are set from the shell
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized.
Exec=/bin/bash --norc --noprofile -i -c '"$SHELL" -i -c scrcpy || read -p "Press any key to quit..."'
Icon=scrcpy
Terminal=true
Type=Application
Categories=Utility;RemoteAccess;
StartupNotify=false

13
app/data/scrcpy.desktop Normal file
View File

@ -0,0 +1,13 @@
[Desktop Entry]
Name=scrcpy
GenericName=Android Remote Control
Comment=Display and control your Android device
# For some users, the PATH or ADB environment variables are set from the shell
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized.
Exec=/bin/sh -c '"$SHELL" -i -c scrcpy'
Icon=scrcpy
Terminal=false
Type=Application
Categories=Utility;RemoteAccess;
StartupNotify=false

View File

@ -47,7 +47,7 @@ arguments=(
'--record-format=[Force recording format]:format:(mp4 mkv)' '--record-format=[Force recording format]:format:(mp4 mkv)'
'--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)'
'--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)'
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
{-S,--turn-screen-off}'[Turn the device screen off immediately]' {-S,--turn-screen-off}'[Turn the device screen off immediately]'
{-t,--show-touches}'[Show physical touches]' {-t,--show-touches}'[Show physical touches]'

View File

@ -37,6 +37,7 @@ src = [
'src/util/net_intr.c', 'src/util/net_intr.c',
'src/util/process.c', 'src/util/process.c',
'src/util/process_intr.c', 'src/util/process_intr.c',
'src/util/rand.c',
'src/util/strbuf.c', 'src/util/strbuf.c',
'src/util/str.c', 'src/util/str.c',
'src/util/term.c', 'src/util/term.c',
@ -170,6 +171,8 @@ check_functions = [
'strdup', 'strdup',
'asprintf', 'asprintf',
'vasprintf', 'vasprintf',
'nrand48',
'jrand48',
] ]
foreach f : check_functions foreach f : check_functions
@ -223,14 +226,26 @@ executable('scrcpy', src,
install: true, install: true,
c_args: []) c_args: [])
# <https://mesonbuild.com/Builtin-options.html#directories>
datadir = get_option('datadir') # by default 'share'
install_man('scrcpy.1') install_man('scrcpy.1')
install_data('data/icon.png', install_data('data/icon.png',
rename: 'scrcpy.png', rename: 'scrcpy.png',
install_dir: 'share/icons/hicolor/256x256/apps') install_dir: join_paths(datadir, 'icons/hicolor/256x256/apps'))
install_data('data/zsh-completion/_scrcpy', install_data('data/zsh-completion/_scrcpy',
install_dir: 'share/zsh/site-functions') install_dir: join_paths(datadir, 'zsh/site-functions'))
install_data('data/bash-completion/scrcpy', install_data('data/bash-completion/scrcpy',
install_dir: 'share/bash-completion/completions') install_dir: join_paths(datadir, 'bash-completion/completions'))
# Desktop entry file for application launchers
if host_machine.system() == 'linux'
# Install a launcher (ex: /usr/local/share/applications/scrcpy.desktop)
install_data('data/scrcpy.desktop',
install_dir: join_paths(datadir, 'applications'))
install_data('data/scrcpy-console.desktop',
install_dir: join_paths(datadir, 'applications'))
endif
### TESTS ### TESTS
@ -245,8 +260,8 @@ if get_option('buildtype') == 'debug'
'src/util/str.c', 'src/util/str.c',
'src/util/strbuf.c', 'src/util/strbuf.c',
]], ]],
['test_buffer_util', [ ['test_binary', [
'tests/test_buffer_util.c', 'tests/test_binary.c',
]], ]],
['test_cbuf', [ ['test_cbuf', [
'tests/test_cbuf.c', 'tests/test_cbuf.c',
@ -255,6 +270,7 @@ if get_option('buildtype') == 'debug'
'tests/test_cli.c', 'tests/test_cli.c',
'src/cli.c', 'src/cli.c',
'src/options.c', 'src/options.c',
'src/util/log.c',
'src/util/net.c', 'src/util/net.c',
'src/util/str.c', 'src/util/str.c',
'src/util/strbuf.c', 'src/util/strbuf.c',

View File

@ -6,10 +6,10 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR" mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR"
DEP_DIR=platform-tools-33.0.1 DEP_DIR=platform-tools-33.0.3
FILENAME=platform-tools_r33.0.1-windows.zip FILENAME=platform-tools_r33.0.3-windows.zip
SHA256SUM=c1f02d42ea24ef4ff2a405ae7370e764ef4546f9b3e4520f5571a00ed5012c42 SHA256SUM=1e59afd40a74c5c0eab0a9fad3f0faf8a674267106e0b19921be9f67081808c2
if [[ -d "$DEP_DIR" ]] if [[ -d "$DEP_DIR" ]]
then then

View File

@ -6,11 +6,11 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR" mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR"
VERSION=5.0.1 VERSION=5.1.2
DEP_DIR=ffmpeg-win64-$VERSION DEP_DIR=ffmpeg-win64-$VERSION
FILENAME=ffmpeg-$VERSION-full_build-shared.7z FILENAME=ffmpeg-$VERSION-full_build-shared.7z
SHA256SUM=ded28435b6f04b74f5ef5a6a13761233bce9e8e9f8ecb0eabe936fd36a778b0c SHA256SUM=d9eb97b72d7cfdae4d0f7eaea59ccffb8c364d67d88018ea715d5e2e193f00e9
if [[ -d "$DEP_DIR" ]] if [[ -d "$DEP_DIR" ]]
then then

View File

@ -6,10 +6,10 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR" mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR"
DEP_DIR=SDL2-2.0.22 DEP_DIR=SDL2-2.26.1
FILENAME=SDL2-devel-2.0.22-mingw.tar.gz FILENAME=SDL2-devel-2.26.1-mingw.tar.gz
SHA256SUM=0e91e35973366aa1e6f81ee368924d9b4f93f9da4d2f2a89ec80b06eadcf23d1 SHA256SUM=aa43e1531a89551f9f9e14b27953a81d4ac946a9e574b5813cd0f2b36e83cc1c
if [[ -d "$DEP_DIR" ]] if [[ -d "$DEP_DIR" ]]
then then

View File

@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe" VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy" VALUE "ProductName", "scrcpy"
VALUE "ProductVersion", "1.24" VALUE "ProductVersion", "1.25"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View File

@ -275,7 +275,7 @@ 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 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. If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting.
.TP .TP
.B \-S, \-\-turn\-screen\-off .B \-S, \-\-turn\-screen\-off

View File

@ -401,6 +401,7 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags,
#define BUFSIZE 65536 #define BUFSIZE 65536
char *buf = malloc(BUFSIZE); char *buf = malloc(BUFSIZE);
if (!buf) { if (!buf) {
LOG_OOM();
return false; return false;
} }
@ -710,5 +711,5 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) {
// It is parsed as a NUL-terminated string // It is parsed as a NUL-terminated string
buf[r] = '\0'; buf[r] = '\0';
return sc_adb_parse_device_ip_from_output(buf); return sc_adb_parse_device_ip(buf);
} }

View File

@ -199,7 +199,7 @@ sc_adb_parse_device_ip_from_line(char *line) {
} }
char * char *
sc_adb_parse_device_ip_from_output(char *str) { sc_adb_parse_device_ip(char *str) {
size_t idx_line = 0; size_t idx_line = 0;
while (str[idx_line] != '\0') { while (str[idx_line] != '\0') {
char *line = &str[idx_line]; char *line = &str[idx_line];

View File

@ -25,6 +25,6 @@ sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec);
* Warning: this function modifies the buffer for optimization purposes. * Warning: this function modifies the buffer for optimization purposes.
*/ */
char * char *
sc_adb_parse_device_ip_from_output(char *str); sc_adb_parse_device_ip(char *str);
#endif #endif

View File

@ -7,8 +7,6 @@
#include "util/net_intr.h" #include "util/net_intr.h"
#include "util/process_intr.h" #include "util/process_intr.h"
#define SC_SOCKET_NAME "scrcpy"
static bool static bool
listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1); return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1);
@ -17,10 +15,11 @@ listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
static bool static bool
enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
struct sc_intr *intr, const char *serial, struct sc_intr *intr, const char *serial,
const char *device_socket_name,
struct sc_port_range port_range) { struct sc_port_range port_range) {
uint16_t port = port_range.first; uint16_t port = port_range.first;
for (;;) { for (;;) {
if (!sc_adb_reverse(intr, serial, SC_SOCKET_NAME, port, if (!sc_adb_reverse(intr, serial, device_socket_name, port,
SC_ADB_NO_STDOUT)) { SC_ADB_NO_STDOUT)) {
// the command itself failed, it will fail on any port // the command itself failed, it will fail on any port
return false; return false;
@ -52,7 +51,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
} }
// failure, disable tunnel and try another port // failure, disable tunnel and try another port
if (!sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME, if (!sc_adb_reverse_remove(intr, serial, device_socket_name,
SC_ADB_NO_STDOUT)) { SC_ADB_NO_STDOUT)) {
LOGW("Could not remove reverse tunnel on port %" PRIu16, port); LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
} }
@ -78,12 +77,13 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
static bool static bool
enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel, enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel,
struct sc_intr *intr, const char *serial, struct sc_intr *intr, const char *serial,
const char *device_socket_name,
struct sc_port_range port_range) { struct sc_port_range port_range) {
tunnel->forward = true; tunnel->forward = true;
uint16_t port = port_range.first; uint16_t port = port_range.first;
for (;;) { for (;;) {
if (sc_adb_forward(intr, serial, port, SC_SOCKET_NAME, if (sc_adb_forward(intr, serial, port, device_socket_name,
SC_ADB_NO_STDOUT)) { SC_ADB_NO_STDOUT)) {
// success // success
tunnel->local_port = port; tunnel->local_port = port;
@ -123,13 +123,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) {
bool bool
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial, struct sc_port_range port_range, const char *serial, const char *device_socket_name,
bool force_adb_forward) { struct sc_port_range port_range, bool force_adb_forward) {
assert(!tunnel->enabled); assert(!tunnel->enabled);
if (!force_adb_forward) { if (!force_adb_forward) {
// Attempt to use "adb reverse" // Attempt to use "adb reverse"
if (enable_tunnel_reverse_any_port(tunnel, intr, serial, port_range)) { if (enable_tunnel_reverse_any_port(tunnel, intr, serial,
device_socket_name, port_range)) {
return true; return true;
} }
@ -139,12 +140,13 @@ sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
LOGW("'adb reverse' failed, fallback to 'adb forward'"); LOGW("'adb reverse' failed, fallback to 'adb forward'");
} }
return enable_tunnel_forward_any_port(tunnel, intr, serial, port_range); return enable_tunnel_forward_any_port(tunnel, intr, serial,
device_socket_name, port_range);
} }
bool bool
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial) { const char *serial, const char *device_socket_name) {
assert(tunnel->enabled); assert(tunnel->enabled);
bool ret; bool ret;
@ -152,7 +154,7 @@ sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
ret = sc_adb_forward_remove(intr, serial, tunnel->local_port, ret = sc_adb_forward_remove(intr, serial, tunnel->local_port,
SC_ADB_NO_STDOUT); SC_ADB_NO_STDOUT);
} else { } else {
ret = sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME, ret = sc_adb_reverse_remove(intr, serial, device_socket_name,
SC_ADB_NO_STDOUT); SC_ADB_NO_STDOUT);
assert(tunnel->server_socket != SC_SOCKET_NONE); assert(tunnel->server_socket != SC_SOCKET_NONE);

View File

@ -34,14 +34,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel);
*/ */
bool bool
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial, struct sc_port_range port_range, const char *serial, const char *device_socket_name,
bool force_adb_forward); struct sc_port_range port_range, bool force_adb_forward);
/** /**
* Close the tunnel * Close the tunnel
*/ */
bool bool
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial); const char *serial, const char *device_socket_name);
#endif #endif

View File

@ -98,7 +98,7 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
sc_clock_estimate(clock, &clock->slope, &clock->offset); sc_clock_estimate(clock, &clock->slope, &clock->offset);
#ifndef SC_CLOCK_NDEBUG #ifndef SC_CLOCK_NDEBUG
LOGD("Clock estimation: %g * pts + %" PRItick, LOGD("Clock estimation: %f * pts + %" PRItick,
clock->slope, clock->offset); clock->slope, clock->offset);
#endif #endif
} }

View File

@ -51,3 +51,47 @@ int vasprintf(char **strp, const char *fmt, va_list ap) {
return len; return len;
} }
#endif #endif
#if !defined(HAVE_NRAND48) || !defined(HAVE_JRAND48)
#define SC_RAND48_MASK UINT64_C(0xFFFFFFFFFFFF) // 48 bits
#define SC_RAND48_A UINT64_C(0x5DEECE66D)
#define SC_RAND48_C 0xB
static inline uint64_t rand_iter48(uint64_t x) {
assert((x & ~SC_RAND48_MASK) == 0);
return (x * SC_RAND48_A + SC_RAND48_C) & SC_RAND48_MASK;
}
static uint64_t rand_iter48_xsubi(unsigned short xsubi[3]) {
uint64_t x = ((uint64_t) xsubi[0] << 32)
| ((uint64_t) xsubi[1] << 16)
| xsubi[2];
x = rand_iter48(x);
xsubi[0] = (x >> 32) & 0XFFFF;
xsubi[1] = (x >> 16) & 0XFFFF;
xsubi[2] = x & 0XFFFF;
return x;
}
#ifndef HAVE_NRAND48
long nrand48(unsigned short xsubi[3]) {
// range [0, 2^31)
return rand_iter48_xsubi(xsubi) >> 17;
}
#endif
#ifndef HAVE_JRAND48
long jrand48(unsigned short xsubi[3]) {
// range [-2^31, 2^31)
union {
uint32_t u;
int32_t i;
} v;
v.u = rand_iter48_xsubi(xsubi) >> 16;
return v.i;
}
#endif
#endif

View File

@ -59,4 +59,12 @@ int asprintf(char **strp, const char *fmt, ...);
int vasprintf(char **strp, const char *fmt, va_list ap); int vasprintf(char **strp, const char *fmt, va_list ap);
#endif #endif
#ifndef HAVE_NRAND48
long nrand48(unsigned short xsubi[3]);
#endif
#ifndef HAVE_JRAND48
long jrand48(unsigned short xsubi[3]);
#endif
#endif #endif

View File

@ -5,7 +5,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "util/buffer_util.h" #include "util/binary.h"
#include "util/log.h" #include "util/log.h"
#include "util/str.h" #include "util/str.h"
@ -37,7 +37,7 @@ static const char *const android_motionevent_action_labels[] = {
"move", "move",
"cancel", "cancel",
"outside", "outside",
"ponter-down", "pointer-down",
"pointer-up", "pointer-up",
"hover-move", "hover-move",
"scroll", "scroll",
@ -61,6 +61,22 @@ static const char *const copy_key_labels[] = {
"cut", "cut",
}; };
static inline const char *
get_well_known_pointer_id_name(uint64_t pointer_id) {
switch (pointer_id) {
case POINTER_ID_MOUSE:
return "mouse";
case POINTER_ID_GENERIC_FINGER:
return "finger";
case POINTER_ID_VIRTUAL_MOUSE:
return "vmouse";
case POINTER_ID_VIRTUAL_FINGER:
return "vfinger";
default:
return NULL;
}
}
static void static void
write_position(uint8_t *buf, const struct sc_position *position) { write_position(uint8_t *buf, const struct sc_position *position) {
sc_write32be(&buf[0], position->point.x); sc_write32be(&buf[0], position->point.x);
@ -78,16 +94,6 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) {
return 4 + len; return 4 + len;
} }
static uint16_t
to_fixed_point_16(float f) {
assert(f >= 0.0f && f <= 1.0f);
uint32_t u = f * 0x1p16f; // 2^16
if (u >= 0xffff) {
u = 0xffff;
}
return (uint16_t) u;
}
size_t size_t
sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
buf[0] = msg->type; buf[0] = msg->type;
@ -109,18 +115,20 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
sc_write64be(&buf[2], msg->inject_touch_event.pointer_id); sc_write64be(&buf[2], msg->inject_touch_event.pointer_id);
write_position(&buf[10], &msg->inject_touch_event.position); write_position(&buf[10], &msg->inject_touch_event.position);
uint16_t pressure = uint16_t pressure =
to_fixed_point_16(msg->inject_touch_event.pressure); sc_float_to_u16fp(msg->inject_touch_event.pressure);
sc_write16be(&buf[22], pressure); sc_write16be(&buf[22], pressure);
sc_write32be(&buf[24], msg->inject_touch_event.buttons); sc_write32be(&buf[24], msg->inject_touch_event.buttons);
return 28; return 28;
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position); write_position(&buf[1], &msg->inject_scroll_event.position);
sc_write32be(&buf[13], int16_t hscroll =
(uint32_t) msg->inject_scroll_event.hscroll); sc_float_to_i16fp(msg->inject_scroll_event.hscroll);
sc_write32be(&buf[17], int16_t vscroll =
(uint32_t) msg->inject_scroll_event.vscroll); sc_float_to_i16fp(msg->inject_scroll_event.vscroll);
sc_write32be(&buf[21], msg->inject_scroll_event.buttons); sc_write16be(&buf[13], (uint16_t) hscroll);
return 25; sc_write16be(&buf[15], (uint16_t) vscroll);
sc_write32be(&buf[17], msg->inject_scroll_event.buttons);
return 21;
case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
buf[1] = msg->inject_keycode.action; buf[1] = msg->inject_keycode.action;
return 2; return 2;
@ -167,11 +175,12 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
int action = msg->inject_touch_event.action int action = msg->inject_touch_event.action
& AMOTION_EVENT_ACTION_MASK; & AMOTION_EVENT_ACTION_MASK;
uint64_t id = msg->inject_touch_event.pointer_id; uint64_t id = msg->inject_touch_event.pointer_id;
if (id == POINTER_ID_MOUSE || id == POINTER_ID_VIRTUAL_FINGER) { const char *pointer_name = get_well_known_pointer_id_name(id);
if (pointer_name) {
// string pointer id // string pointer id
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32 LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
" pressure=%g buttons=%06lx", " pressure=%f buttons=%06lx",
id == POINTER_ID_MOUSE ? "mouse" : "vfinger", pointer_name,
MOTIONEVENT_ACTION_LABEL(action), MOTIONEVENT_ACTION_LABEL(action),
msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.x,
msg->inject_touch_event.position.point.y, msg->inject_touch_event.position.point.y,
@ -180,7 +189,7 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
} else { } else {
// numeric pointer id // numeric pointer id
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%" LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
PRIi32 " pressure=%g buttons=%06lx", PRIi32 " pressure=%f buttons=%06lx",
id, id,
MOTIONEVENT_ACTION_LABEL(action), MOTIONEVENT_ACTION_LABEL(action),
msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.x,
@ -191,8 +200,8 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
break; break;
} }
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%" PRIi32 LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%f"
" vscroll=%" PRIi32 " buttons=%06lx", " vscroll=%f buttons=%06lx",
msg->inject_scroll_event.position.point.x, msg->inject_scroll_event.position.point.x,
msg->inject_scroll_event.position.point.y, msg->inject_scroll_event.position.point.y,
msg->inject_scroll_event.hscroll, msg->inject_scroll_event.hscroll,

View File

@ -18,7 +18,11 @@
#define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14) #define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14)
#define POINTER_ID_MOUSE UINT64_C(-1) #define POINTER_ID_MOUSE UINT64_C(-1)
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2) #define POINTER_ID_GENERIC_FINGER UINT64_C(-2)
// Used for injecting an additional virtual pointer for pinch-to-zoom
#define POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3)
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-4)
enum sc_control_msg_type { enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, SC_CONTROL_MSG_TYPE_INJECT_KEYCODE,
@ -68,8 +72,8 @@ struct sc_control_msg {
} inject_touch_event; } inject_touch_event;
struct { struct {
struct sc_position position; struct sc_position position;
int32_t hscroll; float hscroll;
int32_t vscroll; float vscroll;
enum android_motionevent_buttons buttons; enum android_motionevent_buttons buttons;
} inject_scroll_event; } inject_scroll_event;
struct { struct {

View File

@ -7,7 +7,7 @@
#include "decoder.h" #include "decoder.h"
#include "events.h" #include "events.h"
#include "recorder.h" #include "recorder.h"
#include "util/buffer_util.h" #include "util/binary.h"
#include "util/log.h" #include "util/log.h"
#define SC_PACKET_HEADER_SIZE 12 #define SC_PACKET_HEADER_SIZE 12
@ -37,8 +37,8 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// CK...... ........ ........ ........ ........ ........ ........ ........ // CK...... ........ ........ ........ ........ ........ ........ ........
// ^^<-------------------------------------------------------------------> // ^^<------------------------------------------------------------------->
// || PTS // || PTS
// | `- config packet // | `- key frame
// `-- key frame // `-- config packet
uint8_t header[SC_PACKET_HEADER_SIZE]; uint8_t header[SC_PACKET_HEADER_SIZE];
ssize_t r = net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE); ssize_t r = net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE);
@ -95,29 +95,27 @@ sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// A config packet must not be decoded immediately (it contains no // A config packet must not be decoded immediately (it contains no
// frame); instead, it must be concatenated with the future data packet. // frame); instead, it must be concatenated with the future data packet.
if (demuxer->pending || is_config) { if (demuxer->pending || is_config) {
size_t offset;
if (demuxer->pending) { if (demuxer->pending) {
offset = demuxer->pending->size; size_t offset = demuxer->pending->size;
if (av_grow_packet(demuxer->pending, packet->size)) { if (av_grow_packet(demuxer->pending, packet->size)) {
LOG_OOM(); LOG_OOM();
return false; return false;
} }
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
} else { } else {
offset = 0;
demuxer->pending = av_packet_alloc(); demuxer->pending = av_packet_alloc();
if (!demuxer->pending) { if (!demuxer->pending) {
LOG_OOM(); LOG_OOM();
return false; return false;
} }
if (av_new_packet(demuxer->pending, packet->size)) { if (av_packet_ref(demuxer->pending, packet)) {
LOG_OOM(); LOG_OOM();
av_packet_free(&demuxer->pending); av_packet_free(&demuxer->pending);
return false; return false;
} }
} }
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
if (!is_config) { if (!is_config) {
// prepare the concat packet to send to the decoder // prepare the concat packet to send to the decoder
demuxer->pending->pts = packet->pts; demuxer->pending->pts = packet->pts;

View File

@ -4,7 +4,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "util/buffer_util.h" #include "util/binary.h"
#include "util/log.h" #include "util/log.h"
ssize_t ssize_t

View File

@ -353,18 +353,20 @@ struct sc_mouse_click_event {
struct sc_position position; struct sc_position position;
enum sc_action action; enum sc_action action;
enum sc_mouse_button button; enum sc_mouse_button button;
uint64_t pointer_id;
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
}; };
struct sc_mouse_scroll_event { struct sc_mouse_scroll_event {
struct sc_position position; struct sc_position position;
int32_t hscroll; float hscroll;
int32_t vscroll; float vscroll;
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
}; };
struct sc_mouse_motion_event { struct sc_mouse_motion_event {
struct sc_position position; struct sc_position position;
uint64_t pointer_id;
int32_t xrel; int32_t xrel;
int32_t yrel; int32_t yrel;
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values uint8_t buttons_state; // bitwise-OR of sc_mouse_button values

View File

@ -335,7 +335,9 @@ simulate_virtual_finger(struct sc_input_manager *im,
msg.inject_touch_event.action = action; msg.inject_touch_event.action = action;
msg.inject_touch_event.position.screen_size = im->screen->frame_size; msg.inject_touch_event.position.screen_size = im->screen->frame_size;
msg.inject_touch_event.position.point = point; msg.inject_touch_event.position.point = point;
msg.inject_touch_event.pointer_id = POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pointer_id =
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
: POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
msg.inject_touch_event.buttons = 0; msg.inject_touch_event.buttons = 0;
@ -564,6 +566,8 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
event->x, event->x,
event->y), event->y),
}, },
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
: POINTER_ID_GENERIC_FINGER,
.xrel = event->xrel, .xrel = event->xrel,
.yrel = event->yrel, .yrel = event->yrel,
.buttons_state = .buttons_state =
@ -687,6 +691,8 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
}, },
.action = sc_action_from_sdl_mousebutton_type(event->type), .action = sc_action_from_sdl_mousebutton_type(event->type),
.button = sc_mouse_button_from_sdl(event->button), .button = sc_mouse_button_from_sdl(event->button),
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
: POINTER_ID_GENERIC_FINGER,
.buttons_state = .buttons_state =
sc_mouse_buttons_state_from_sdl(sdl_buttons_state, sc_mouse_buttons_state_from_sdl(sdl_buttons_state,
im->forward_all_clicks), im->forward_all_clicks),
@ -747,8 +753,13 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
.point = sc_screen_convert_window_to_frame_coords(im->screen, .point = sc_screen_convert_window_to_frame_coords(im->screen,
mouse_x, mouse_y), mouse_x, mouse_y),
}, },
.hscroll = event->x, #if SDL_VERSION_ATLEAST(2, 0, 18)
.vscroll = event->y, .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f),
.vscroll = CLAMP(event->preciseY, -1.0f, 1.0f),
#else
.hscroll = CLAMP(event->x, -1, 1),
.vscroll = CLAMP(event->y, -1, 1),
#endif
.buttons_state = .buttons_state =
sc_mouse_buttons_state_from_sdl(buttons, im->forward_all_clicks), sc_mouse_buttons_state_from_sdl(buttons, im->forward_all_clicks),
}; };

View File

@ -4,6 +4,10 @@
#include <stdbool.h> #include <stdbool.h>
#include <unistd.h> #include <unistd.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#ifdef _WIN32
#include <windows.h>
#include "util/str.h"
#endif
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
# include <libavdevice/avdevice.h> # include <libavdevice/avdevice.h>
#endif #endif
@ -18,8 +22,8 @@
#include "version.h" #include "version.h"
int int
main(int argc, char *argv[]) { main_scrcpy(int argc, char *argv[]) {
#ifdef __WINDOWS__ #ifdef _WIN32
// disable buffering, we want logs immediately // disable buffering, we want logs immediately
// even line buffering (setvbuf() with mode _IOLBF) is not sufficient // even line buffering (setvbuf() with mode _IOLBF) is not sufficient
setbuf(stdout, NULL); setbuf(stdout, NULL);
@ -80,3 +84,52 @@ main(int argc, char *argv[]) {
return ret; return ret;
} }
int
main(int argc, char *argv[]) {
#ifndef _WIN32
return main_scrcpy(argc, argv);
#else
(void) argc;
(void) argv;
int wargc;
wchar_t **wargv = CommandLineToArgvW(GetCommandLineW(), &wargc);
if (!wargv) {
LOG_OOM();
return SCRCPY_EXIT_FAILURE;
}
char **argv_utf8 = malloc((wargc + 1) * sizeof(*argv_utf8));
if (!argv_utf8) {
LOG_OOM();
LocalFree(wargv);
return SCRCPY_EXIT_FAILURE;
}
argv_utf8[wargc] = NULL;
for (int i = 0; i < wargc; ++i) {
argv_utf8[i] = sc_str_from_wchars(wargv[i]);
if (!argv_utf8[i]) {
LOG_OOM();
for (int j = 0; j < i; ++j) {
free(argv_utf8[j]);
}
LocalFree(wargv);
free(argv_utf8);
return SCRCPY_EXIT_FAILURE;
}
}
LocalFree(wargv);
int ret = main_scrcpy(wargc, argv_utf8);
for (int i = 0; i < wargc; ++i) {
free(argv_utf8[i]);
}
free(argv_utf8);
return ret;
#endif
}

View File

@ -69,7 +69,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp,
.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
.inject_touch_event = { .inject_touch_event = {
.action = AMOTION_EVENT_ACTION_MOVE, .action = AMOTION_EVENT_ACTION_MOVE,
.pointer_id = POINTER_ID_MOUSE, .pointer_id = event->pointer_id,
.position = event->position, .position = event->position,
.pressure = 1.f, .pressure = 1.f,
.buttons = convert_mouse_buttons(event->buttons_state), .buttons = convert_mouse_buttons(event->buttons_state),
@ -90,7 +90,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
.inject_touch_event = { .inject_touch_event = {
.action = convert_mouse_action(event->action), .action = convert_mouse_action(event->action),
.pointer_id = POINTER_ID_MOUSE, .pointer_id = event->pointer_id,
.position = event->position, .position = event->position,
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f, .pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
.buttons = convert_mouse_buttons(event->buttons_state), .buttons = convert_mouse_buttons(event->buttons_state),

View File

@ -32,6 +32,7 @@
#include "util/acksync.h" #include "util/acksync.h"
#include "util/log.h" #include "util/log.h"
#include "util/net.h" #include "util/net.h"
#include "util/rand.h"
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
# include "v4l2_sink.h" # include "v4l2_sink.h"
#endif #endif
@ -265,6 +266,14 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
// event // event
} }
static uint32_t
scrcpy_generate_uid() {
struct sc_rand rand;
sc_rand_init(&rand);
// Only use 31 bits to avoid issues with signed values on the Java-side
return sc_rand_u32(&rand) & 0x7FFFFFFF;
}
enum scrcpy_exit_code enum scrcpy_exit_code
scrcpy(struct scrcpy_options *options) { scrcpy(struct scrcpy_options *options) {
static struct scrcpy scrcpy; static struct scrcpy scrcpy;
@ -298,7 +307,10 @@ scrcpy(struct scrcpy_options *options) {
struct sc_acksync *acksync = NULL; struct sc_acksync *acksync = NULL;
uint32_t uid = scrcpy_generate_uid();
struct sc_server_params params = { struct sc_server_params params = {
.uid = uid,
.req_serial = options->serial, .req_serial = options->serial,
.select_usb = options->select_usb, .select_usb = options->select_usb,
.select_tcpip = options->select_tcpip, .select_tcpip = options->select_tcpip,

View File

@ -19,6 +19,9 @@
#define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME #define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME
#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" #define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
#define SC_ADB_PORT_DEFAULT 5555
#define SC_SOCKET_NAME_PREFIX "scrcpy_"
static char * static char *
get_server_path(void) { get_server_path(void) {
#ifdef __WINDOWS__ #ifdef __WINDOWS__
@ -195,6 +198,7 @@ execute_server(struct sc_server *server,
cmd[count++] = p; \ cmd[count++] = p; \
} }
ADD_PARAM("uid=%08x", params->uid);
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
@ -362,6 +366,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
} }
server->serial = NULL; server->serial = NULL;
server->device_socket_name = NULL;
server->stopped = false; server->stopped = false;
server->video_socket = SC_SOCKET_NONE; server->video_socket = SC_SOCKET_NONE;
@ -461,7 +466,8 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
} }
// we don't need the adb tunnel anymore // we don't need the adb tunnel anymore
sc_adb_tunnel_close(tunnel, &server->intr, serial); sc_adb_tunnel_close(tunnel, &server->intr, serial,
server->device_socket_name);
// The sockets will be closed on stop if device_read_info() fails // The sockets will be closed on stop if device_read_info() fails
bool ok = device_read_info(&server->intr, video_socket, info); bool ok = device_read_info(&server->intr, video_socket, info);
@ -492,7 +498,8 @@ fail:
if (tunnel->enabled) { if (tunnel->enabled) {
// Always leave this function with tunnel disabled // Always leave this function with tunnel disabled
sc_adb_tunnel_close(tunnel, &server->intr, serial); sc_adb_tunnel_close(tunnel, &server->intr, serial,
server->device_socket_name);
} }
return false; return false;
@ -513,27 +520,36 @@ sc_server_on_terminated(void *userdata) {
LOGD("Server terminated"); LOGD("Server terminated");
} }
static bool static uint16_t
is_tcpip_mode_enabled(struct sc_server *server, const char *serial) { get_adb_tcp_port(struct sc_server *server, const char *serial) {
struct sc_intr *intr = &server->intr; struct sc_intr *intr = &server->intr;
char *current_port = char *current_port =
sc_adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT); sc_adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT);
if (!current_port) { if (!current_port) {
return false; return 0;
} }
// Is the device is listening on TCP on port 5555? long value;
bool enabled = !strcmp("5555", current_port); bool ok = sc_str_parse_integer(current_port, &value);
free(current_port); free(current_port);
return enabled; if (!ok) {
return 0;
}
if (value < 0 || value > 0xFFFF) {
return 0;
}
return value;
} }
static bool static bool
wait_tcpip_mode_enabled(struct sc_server *server, const char *serial, wait_tcpip_mode_enabled(struct sc_server *server, const char *serial,
unsigned attempts, sc_tick delay) { uint16_t expected_port, unsigned attempts,
if (is_tcpip_mode_enabled(server, serial)) { sc_tick delay) {
LOGI("TCP/IP mode enabled"); uint16_t adb_port = get_adb_tcp_port(server, serial);
if (adb_port == expected_port) {
return true; return true;
} }
@ -547,28 +563,23 @@ wait_tcpip_mode_enabled(struct sc_server *server, const char *serial,
return false; return false;
} }
if (is_tcpip_mode_enabled(server, serial)) { adb_port = get_adb_tcp_port(server, serial);
LOGI("TCP/IP mode enabled"); if (adb_port == expected_port) {
return true; return true;
} }
} while (--attempts); } while (--attempts);
return false; return false;
} }
char * static char *
append_port_5555(const char *ip) { append_port(const char *ip, uint16_t port) {
size_t len = strlen(ip); char *ip_port;
int ret = asprintf(&ip_port, "%s:%" PRIu16, ip, port);
// sizeof counts the final '\0' if (ret == -1) {
char *ip_port = malloc(len + sizeof(":5555"));
if (!ip_port) {
LOG_OOM(); LOG_OOM();
return NULL; return NULL;
} }
memcpy(ip_port, ip, len);
memcpy(ip_port + len, ":5555", sizeof(":5555"));
return ip_port; return ip_port;
} }
@ -586,34 +597,36 @@ sc_server_switch_to_tcpip(struct sc_server *server, const char *serial) {
return NULL; return NULL;
} }
char *ip_port = append_port_5555(ip); uint16_t adb_port = get_adb_tcp_port(server, serial);
free(ip); if (adb_port) {
if (!ip_port) { LOGI("TCP/IP mode already enabled on port %" PRIu16, adb_port);
return NULL; } else {
} LOGI("Enabling TCP/IP mode on port " SC_STR(SC_ADB_PORT_DEFAULT) "...");
bool tcp_mode = is_tcpip_mode_enabled(server, serial); bool ok = sc_adb_tcpip(intr, serial, SC_ADB_PORT_DEFAULT,
SC_ADB_NO_STDOUT);
if (!tcp_mode) {
bool ok = sc_adb_tcpip(intr, serial, 5555, SC_ADB_NO_STDOUT);
if (!ok) { if (!ok) {
LOGE("Could not restart adbd in TCP/IP mode"); LOGE("Could not restart adbd in TCP/IP mode");
goto error; free(ip);
return NULL;
} }
unsigned attempts = 40; unsigned attempts = 40;
sc_tick delay = SC_TICK_FROM_MS(250); sc_tick delay = SC_TICK_FROM_MS(250);
ok = wait_tcpip_mode_enabled(server, serial, attempts, delay); ok = wait_tcpip_mode_enabled(server, serial, SC_ADB_PORT_DEFAULT,
attempts, delay);
if (!ok) { if (!ok) {
goto error; free(ip);
return NULL;
} }
adb_port = SC_ADB_PORT_DEFAULT;
LOGI("TCP/IP mode enabled on port " SC_STR(SC_ADB_PORT_DEFAULT));
} }
char *ip_port = append_port(ip, adb_port);
free(ip);
return ip_port; return ip_port;
error:
free(ip_port);
return NULL;
} }
static bool static bool
@ -640,7 +653,8 @@ sc_server_configure_tcpip_known_address(struct sc_server *server,
const char *addr) { const char *addr) {
// Append ":5555" if no port is present // Append ":5555" if no port is present
bool contains_port = strchr(addr, ':'); bool contains_port = strchr(addr, ':');
char *ip_port = contains_port ? strdup(addr) : append_port_5555(addr); char *ip_port = contains_port ? strdup(addr)
: append_port(addr, SC_ADB_PORT_DEFAULT);
if (!ip_port) { if (!ip_port) {
LOG_OOM(); LOG_OOM();
return false; return false;
@ -755,13 +769,23 @@ run_server(void *data) {
assert(serial); assert(serial);
LOGD("Device serial: %s", serial); LOGD("Device serial: %s", serial);
int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x",
params->uid);
if (r == -1) {
LOG_OOM();
goto error_connection_failed;
}
assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8);
assert(server->device_socket_name);
ok = push_server(&server->intr, serial); ok = push_server(&server->intr, serial);
if (!ok) { if (!ok) {
goto error_connection_failed; goto error_connection_failed;
} }
ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial, ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial,
params->port_range, params->force_adb_forward); server->device_socket_name, params->port_range,
params->force_adb_forward);
if (!ok) { if (!ok) {
goto error_connection_failed; goto error_connection_failed;
} }
@ -769,7 +793,8 @@ run_server(void *data) {
// server will connect to our server socket // server will connect to our server socket
sc_pid pid = execute_server(server, params); sc_pid pid = execute_server(server, params);
if (pid == SC_PROCESS_NONE) { if (pid == SC_PROCESS_NONE) {
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial); sc_adb_tunnel_close(&server->tunnel, &server->intr, serial,
server->device_socket_name);
goto error_connection_failed; goto error_connection_failed;
} }
@ -781,7 +806,8 @@ run_server(void *data) {
if (!ok) { if (!ok) {
sc_process_terminate(pid); sc_process_terminate(pid);
sc_process_wait(pid, true); // ignore exit code sc_process_wait(pid, true); // ignore exit code
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial); sc_adb_tunnel_close(&server->tunnel, &server->intr, serial,
server->device_socket_name);
goto error_connection_failed; goto error_connection_failed;
} }
@ -875,6 +901,7 @@ sc_server_destroy(struct sc_server *server) {
} }
free(server->serial); free(server->serial);
free(server->device_socket_name);
sc_server_params_destroy(&server->params); sc_server_params_destroy(&server->params);
sc_intr_destroy(&server->intr); sc_intr_destroy(&server->intr);
sc_cond_destroy(&server->cond_stopped); sc_cond_destroy(&server->cond_stopped);

View File

@ -22,6 +22,7 @@ struct sc_server_info {
}; };
struct sc_server_params { struct sc_server_params {
uint32_t uid;
const char *req_serial; const char *req_serial;
enum sc_log_level log_level; enum sc_log_level log_level;
const char *crop; const char *crop;
@ -54,6 +55,7 @@ struct sc_server {
// The internal allocated strings are copies owned by the server // The internal allocated strings are copies owned by the server
struct sc_server_params params; struct sc_server_params params;
char *serial; char *serial;
char *device_socket_name;
sc_thread thread; sc_thread thread;
struct sc_server_info info; // initialized once connected struct sc_server_info info; // initialized once connected

View File

@ -1,51 +0,0 @@
#ifndef STREAM_H
#define STREAM_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include "trait/packet_sink.h"
#include "util/net.h"
#include "util/thread.h"
#define STREAM_MAX_SINKS 2
struct stream {
sc_socket socket;
sc_thread thread;
struct sc_packet_sink *sinks[STREAM_MAX_SINKS];
unsigned sink_count;
AVCodecContext *codec_ctx;
AVCodecParserContext *parser;
// successive packets may need to be concatenated, until a non-config
// packet is available
AVPacket *pending;
const struct stream_callbacks *cbs;
void *cbs_userdata;
};
struct stream_callbacks {
void (*on_eos)(struct stream *stream, void *userdata);
};
void
stream_init(struct stream *stream, sc_socket socket,
const struct stream_callbacks *cbs, void *cbs_userdata);
void
stream_add_sink(struct stream *stream, struct sc_packet_sink *sink);
bool
stream_start(struct stream *stream);
void
stream_join(struct stream *stream);
#endif

View File

@ -92,8 +92,14 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags,
close(in[0]); close(in[0]);
} }
close(in[1]); close(in[1]);
} else {
int devnull = open("/dev/null", O_RDONLY | O_CREAT, 0666);
if (devnull != -1) {
dup2(devnull, STDIN_FILENO);
} else {
LOGE("Could not open /dev/null for stdin");
}
} }
// Do not close stdin in the child process, this makes adb fail on Linux
if (pout) { if (pout) {
if (out[1] != STDOUT_FILENO) { if (out[1] != STDOUT_FILENO) {
@ -102,8 +108,12 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags,
} }
close(out[0]); close(out[0]);
} else if (!inherit_stdout) { } else if (!inherit_stdout) {
// Close stdout in the child process int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666);
close(STDOUT_FILENO); if (devnull != -1) {
dup2(devnull, STDOUT_FILENO);
} else {
LOGE("Could not open /dev/null for stdout");
}
} }
if (perr) { if (perr) {
@ -113,8 +123,12 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags,
} }
close(err[0]); close(err[0]);
} else if (!inherit_stderr) { } else if (!inherit_stderr) {
// Close stderr in the child process int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666);
close(STDERR_FILENO); if (devnull != -1) {
dup2(devnull, STDERR_FILENO);
} else {
LOGE("Could not open /dev/null for stderr");
}
} }
close(internal[0]); close(internal[0]);

View File

@ -23,6 +23,11 @@ read_string(libusb_device_handle *handle, uint8_t desc_index) {
// When non-negative, 'result' contains the number of bytes written // When non-negative, 'result' contains the number of bytes written
char *s = malloc(result + 1); char *s = malloc(result + 1);
if (!s) {
LOG_OOM();
return NULL;
}
memcpy(s, buffer, result); memcpy(s, buffer, result);
s[result] = '\0'; s[result] = '\0';
return s; return s;

View File

@ -1,8 +1,9 @@
#ifndef SC_BUFFER_UTIL_H #ifndef SC_BINARY_H
#define SC_BUFFER_UTIL_H #define SC_BINARY_H
#include "common.h" #include "common.h"
#include <assert.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
@ -43,4 +44,33 @@ sc_read64be(const uint8_t *buf) {
return ((uint64_t) msb << 32) | lsb; return ((uint64_t) msb << 32) | lsb;
} }
/**
* Convert a float between 0 and 1 to an unsigned 16-bit fixed-point value
*/
static inline uint16_t
sc_float_to_u16fp(float f) {
assert(f >= 0.0f && f <= 1.0f);
uint32_t u = f * 0x1p16f; // 2^16
if (u >= 0xffff) {
assert(u == 0x10000); // for f == 1.0f
u = 0xffff;
}
return (uint16_t) u;
}
/**
* Convert a float between -1 and 1 to a signed 16-bit fixed-point value
*/
static inline int16_t
sc_float_to_i16fp(float f) {
assert(f >= -1.0f && f <= 1.0f);
int32_t i = f * 0x1p15f; // 2^15
assert(i >= -0x8000);
if (i >= 0x7fff) {
assert(i == 0x8000); // for f == 1.0f
i = 0x7fff;
}
return (int16_t) i;
}
#endif #endif

View File

@ -3,11 +3,10 @@
#include <assert.h> #include <assert.h>
#include <errno.h> #include <errno.h>
#include <stdio.h> #include <stdio.h>
#include <SDL2/SDL_platform.h>
#include "log.h" #include "log.h"
#ifdef __WINDOWS__ #ifdef _WIN32
# include <ws2tcpip.h> # include <ws2tcpip.h>
typedef int socklen_t; typedef int socklen_t;
typedef SOCKET sc_raw_socket; typedef SOCKET sc_raw_socket;
@ -29,7 +28,7 @@
bool bool
net_init(void) { net_init(void) {
#ifdef __WINDOWS__ #ifdef _WIN32
WSADATA wsa; WSADATA wsa;
int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0;
if (res < 0) { if (res < 0) {
@ -42,14 +41,14 @@ net_init(void) {
void void
net_cleanup(void) { net_cleanup(void) {
#ifdef __WINDOWS__ #ifdef _WIN32
WSACleanup(); WSACleanup();
#endif #endif
} }
static inline sc_socket static inline sc_socket
wrap(sc_raw_socket sock) { wrap(sc_raw_socket sock) {
#ifdef __WINDOWS__ #ifdef _WIN32
if (sock == INVALID_SOCKET) { if (sock == INVALID_SOCKET) {
return SC_SOCKET_NONE; return SC_SOCKET_NONE;
} }
@ -72,7 +71,7 @@ wrap(sc_raw_socket sock) {
static inline sc_raw_socket static inline sc_raw_socket
unwrap(sc_socket socket) { unwrap(sc_socket socket) {
#ifdef __WINDOWS__ #ifdef _WIN32
if (socket == SC_SOCKET_NONE) { if (socket == SC_SOCKET_NONE) {
return INVALID_SOCKET; return INVALID_SOCKET;
} }
@ -160,8 +159,8 @@ net_connect(sc_socket socket, uint32_t addr, uint16_t port) {
} }
bool bool
net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog) { net_listen(sc_socket server_socket, uint32_t addr, uint16_t port, int backlog) {
sc_raw_socket raw_sock = unwrap(socket); sc_raw_socket raw_sock = unwrap(server_socket);
int reuse = 1; int reuse = 1;
if (setsockopt(raw_sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, if (setsockopt(raw_sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse,
@ -248,7 +247,7 @@ net_interrupt(sc_socket socket) {
sc_raw_socket raw_sock = unwrap(socket); sc_raw_socket raw_sock = unwrap(socket);
#ifdef __WINDOWS__ #ifdef _WIN32
if (!atomic_flag_test_and_set(&socket->closed)) { if (!atomic_flag_test_and_set(&socket->closed)) {
return !closesocket(raw_sock); return !closesocket(raw_sock);
} }
@ -262,7 +261,7 @@ bool
net_close(sc_socket socket) { net_close(sc_socket socket) {
sc_raw_socket raw_sock = unwrap(socket); sc_raw_socket raw_sock = unwrap(socket);
#ifdef __WINDOWS__ #ifdef _WIN32
bool ret = true; bool ret = true;
if (!atomic_flag_test_and_set(&socket->closed)) { if (!atomic_flag_test_and_set(&socket->closed)) {
ret = !closesocket(raw_sock); ret = !closesocket(raw_sock);

View File

@ -5,9 +5,8 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <SDL2/SDL_platform.h>
#ifdef __WINDOWS__ #ifdef _WIN32
# include <winsock2.h> # include <winsock2.h>
# include <stdatomic.h> # include <stdatomic.h>
@ -17,7 +16,7 @@
atomic_flag closed; atomic_flag closed;
} *sc_socket; } *sc_socket;
#else // not __WINDOWS__ #else // not _WIN32
# include <sys/socket.h> # include <sys/socket.h>
# define SC_SOCKET_NONE -1 # define SC_SOCKET_NONE -1
@ -40,7 +39,7 @@ bool
net_connect(sc_socket socket, uint32_t addr, uint16_t port); net_connect(sc_socket socket, uint32_t addr, uint16_t port);
bool bool
net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog); net_listen(sc_socket server_socket, uint32_t addr, uint16_t port, int backlog);
sc_socket sc_socket
net_accept(sc_socket server_socket); net_accept(sc_socket server_socket);

View File

@ -15,14 +15,14 @@ net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr,
} }
bool bool
net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr,
uint16_t port, int backlog) { uint16_t port, int backlog) {
if (!sc_intr_set_socket(intr, socket)) { if (!sc_intr_set_socket(intr, server_socket)) {
// Already interrupted // Already interrupted
return false; return false;
} }
bool ret = net_listen(socket, addr, port, backlog); bool ret = net_listen(server_socket, addr, port, backlog);
sc_intr_set_socket(intr, SC_SOCKET_NONE); sc_intr_set_socket(intr, SC_SOCKET_NONE);
return ret; return ret;

View File

@ -11,7 +11,7 @@ net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr,
uint16_t port); uint16_t port);
bool bool
net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr,
uint16_t port, int backlog); uint16_t port, int backlog);
sc_socket sc_socket

24
app/src/util/rand.c Normal file
View File

@ -0,0 +1,24 @@
#include "rand.h"
#include <stdlib.h>
#include "tick.h"
void sc_rand_init(struct sc_rand *rand) {
sc_tick seed = sc_tick_now(); // microsecond precision
rand->xsubi[0] = (seed >> 32) & 0xFFFF;
rand->xsubi[1] = (seed >> 16) & 0xFFFF;
rand->xsubi[2] = seed & 0xFFFF;
}
uint32_t sc_rand_u32(struct sc_rand *rand) {
// jrand returns a value in range [-2^31, 2^31]
// conversion from signed to unsigned is well-defined to wrap-around
return jrand48(rand->xsubi);
}
uint64_t sc_rand_u64(struct sc_rand *rand) {
uint32_t msb = sc_rand_u32(rand);
uint32_t lsb = sc_rand_u32(rand);
return ((uint64_t) msb << 32) | lsb;
}

16
app/src/util/rand.h Normal file
View File

@ -0,0 +1,16 @@
#ifndef SC_RAND_H
#define SC_RAND_H
#include "common.h"
#include <inttypes.h>
struct sc_rand {
unsigned short xsubi[3];
};
void sc_rand_init(struct sc_rand *rand);
uint32_t sc_rand_u32(struct sc_rand *rand);
uint64_t sc_rand_u64(struct sc_rand *rand);
#endif

View File

@ -6,6 +6,10 @@
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
/* Stringify a numeric value */
#define SC_STR(s) SC_XSTR(s)
#define SC_XSTR(s) #s
/** /**
* Like strncpy(), except: * Like strncpy(), except:
* - it copies at most n-1 chars * - it copies at most n-1 chars

View File

@ -1,6 +1,7 @@
#include "thread.h" #include "thread.h"
#include <assert.h> #include <assert.h>
#include <string.h>
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
#include "log.h" #include "log.h"

View File

@ -6,6 +6,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
// Adapted from vlc_vector: // Adapted from vlc_vector:
// <https://code.videolan.org/videolan/vlc/-/blob/0857947abaed9c89810cd96353aaa1b7e6ba3b0d/include/vlc_vector.h> // <https://code.videolan.org/videolan/vlc/-/blob/0857947abaed9c89810cd96353aaa1b7e6ba3b0d/include/vlc_vector.h>

View File

@ -5,7 +5,7 @@
#include "adb/adb_device.h" #include "adb/adb_device.h"
#include "adb/adb_parser.h" #include "adb/adb_parser.h"
static void test_adb_devices() { static void test_adb_devices(void) {
char output[] = char output[] =
"List of devices attached\n" "List of devices attached\n"
"0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel "
@ -31,7 +31,7 @@ static void test_adb_devices() {
sc_adb_devices_destroy(&vec); sc_adb_devices_destroy(&vec);
} }
static void test_adb_devices_cr() { static void test_adb_devices_cr(void) {
char output[] = char output[] =
"List of devices attached\r\n" "List of devices attached\r\n"
"0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel "
@ -57,7 +57,7 @@ static void test_adb_devices_cr() {
sc_adb_devices_destroy(&vec); sc_adb_devices_destroy(&vec);
} }
static void test_adb_devices_daemon_start() { static void test_adb_devices_daemon_start(void) {
char output[] = char output[] =
"* daemon not running; starting now at tcp:5037\n" "* daemon not running; starting now at tcp:5037\n"
"* daemon started successfully\n" "* daemon started successfully\n"
@ -78,7 +78,7 @@ static void test_adb_devices_daemon_start() {
sc_adb_devices_destroy(&vec); sc_adb_devices_destroy(&vec);
} }
static void test_adb_devices_daemon_start_mixed() { static void test_adb_devices_daemon_start_mixed(void) {
char output[] = char output[] =
"List of devices attached\n" "List of devices attached\n"
"adb server version (41) doesn't match this client (39); killing...\n" "adb server version (41) doesn't match this client (39); killing...\n"
@ -105,7 +105,7 @@ static void test_adb_devices_daemon_start_mixed() {
sc_adb_devices_destroy(&vec); sc_adb_devices_destroy(&vec);
} }
static void test_adb_devices_without_eol() { static void test_adb_devices_without_eol(void) {
char output[] = char output[] =
"List of devices attached\n" "List of devices attached\n"
"0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel "
@ -124,7 +124,7 @@ static void test_adb_devices_without_eol() {
sc_adb_devices_destroy(&vec); sc_adb_devices_destroy(&vec);
} }
static void test_adb_devices_without_header() { static void test_adb_devices_without_header(void) {
char output[] = char output[] =
"0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel "
"device:MyDevice transport_id:1\n"; "device:MyDevice transport_id:1\n";
@ -134,7 +134,7 @@ static void test_adb_devices_without_header() {
assert(!ok); assert(!ok);
} }
static void test_adb_devices_corrupted() { static void test_adb_devices_corrupted(void) {
char output[] = char output[] =
"List of devices attached\n" "List of devices attached\n"
"corrupted_garbage\n"; "corrupted_garbage\n";
@ -145,7 +145,7 @@ static void test_adb_devices_corrupted() {
assert(vec.size == 0); assert(vec.size == 0);
} }
static void test_adb_devices_spaces() { static void test_adb_devices_spaces(void) {
char output[] = char output[] =
"List of devices attached\n" "List of devices attached\n"
"0123456789abcdef unauthorized usb:1-4 transport_id:3\n"; "0123456789abcdef unauthorized usb:1-4 transport_id:3\n";
@ -163,81 +163,81 @@ static void test_adb_devices_spaces() {
sc_adb_devices_destroy(&vec); sc_adb_devices_destroy(&vec);
} }
static void test_get_ip_single_line() { static void test_get_ip_single_line(void) {
char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.12.34\r\r\n"; "192.168.12.34\r\r\n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route); char *ip = sc_adb_parse_device_ip(ip_route);
assert(ip); assert(ip);
assert(!strcmp(ip, "192.168.12.34")); assert(!strcmp(ip, "192.168.12.34"));
free(ip); free(ip);
} }
static void test_get_ip_single_line_without_eol() { static void test_get_ip_single_line_without_eol(void) {
char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.12.34"; "192.168.12.34";
char *ip = sc_adb_parse_device_ip_from_output(ip_route); char *ip = sc_adb_parse_device_ip(ip_route);
assert(ip); assert(ip);
assert(!strcmp(ip, "192.168.12.34")); assert(!strcmp(ip, "192.168.12.34"));
free(ip); free(ip);
} }
static void test_get_ip_single_line_with_trailing_space() { static void test_get_ip_single_line_with_trailing_space(void) {
char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.12.34 \n"; "192.168.12.34 \n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route); char *ip = sc_adb_parse_device_ip(ip_route);
assert(ip); assert(ip);
assert(!strcmp(ip, "192.168.12.34")); assert(!strcmp(ip, "192.168.12.34"));
free(ip); free(ip);
} }
static void test_get_ip_multiline_first_ok() { static void test_get_ip_multiline_first_ok(void) {
char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.1.2\r\n" "192.168.1.2\r\n"
"10.0.0.0/24 dev rmnet proto kernel scope link src " "10.0.0.0/24 dev rmnet proto kernel scope link src "
"10.0.0.2\r\n"; "10.0.0.2\r\n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route); char *ip = sc_adb_parse_device_ip(ip_route);
assert(ip); assert(ip);
assert(!strcmp(ip, "192.168.1.2")); assert(!strcmp(ip, "192.168.1.2"));
free(ip); free(ip);
} }
static void test_get_ip_multiline_second_ok() { static void test_get_ip_multiline_second_ok(void) {
char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src " char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src "
"10.0.0.3\r\n" "10.0.0.3\r\n"
"192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.1.3\r\n"; "192.168.1.3\r\n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route); char *ip = sc_adb_parse_device_ip(ip_route);
assert(ip); assert(ip);
assert(!strcmp(ip, "192.168.1.3")); assert(!strcmp(ip, "192.168.1.3"));
free(ip); free(ip);
} }
static void test_get_ip_no_wlan() { static void test_get_ip_no_wlan(void) {
char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src "
"192.168.12.34\r\r\n"; "192.168.12.34\r\r\n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route); char *ip = sc_adb_parse_device_ip(ip_route);
assert(!ip); assert(!ip);
} }
static void test_get_ip_no_wlan_without_eol() { static void test_get_ip_no_wlan_without_eol(void) {
char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src "
"192.168.12.34"; "192.168.12.34";
char *ip = sc_adb_parse_device_ip_from_output(ip_route); char *ip = sc_adb_parse_device_ip(ip_route);
assert(!ip); assert(!ip);
} }
static void test_get_ip_truncated() { static void test_get_ip_truncated(void) {
char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src "
"\n"; "\n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route); char *ip = sc_adb_parse_device_ip(ip_route);
assert(!ip); assert(!ip);
} }
@ -262,4 +262,6 @@ int main(int argc, char *argv[]) {
test_get_ip_no_wlan(); test_get_ip_no_wlan();
test_get_ip_no_wlan_without_eol(); test_get_ip_no_wlan_without_eol();
test_get_ip_truncated(); test_get_ip_truncated();
return 0;
} }

114
app/tests/test_binary.c Normal file
View File

@ -0,0 +1,114 @@
#include "common.h"
#include <assert.h>
#include "util/binary.h"
static void test_write16be(void) {
uint16_t val = 0xABCD;
uint8_t buf[2];
sc_write16be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
}
static void test_write32be(void) {
uint32_t val = 0xABCD1234;
uint8_t buf[4];
sc_write32be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
}
static void test_write64be(void) {
uint64_t val = 0xABCD1234567890EF;
uint8_t buf[8];
sc_write64be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
assert(buf[4] == 0x56);
assert(buf[5] == 0x78);
assert(buf[6] == 0x90);
assert(buf[7] == 0xEF);
}
static void test_read16be(void) {
uint8_t buf[2] = {0xAB, 0xCD};
uint16_t val = sc_read16be(buf);
assert(val == 0xABCD);
}
static void test_read32be(void) {
uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34};
uint32_t val = sc_read32be(buf);
assert(val == 0xABCD1234);
}
static void test_read64be(void) {
uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34,
0x56, 0x78, 0x90, 0xEF};
uint64_t val = sc_read64be(buf);
assert(val == 0xABCD1234567890EF);
}
static void test_float_to_u16fp(void) {
assert(sc_float_to_u16fp(0.0f) == 0);
assert(sc_float_to_u16fp(0.03125f) == 0x800);
assert(sc_float_to_u16fp(0.0625f) == 0x1000);
assert(sc_float_to_u16fp(0.125f) == 0x2000);
assert(sc_float_to_u16fp(0.25f) == 0x4000);
assert(sc_float_to_u16fp(0.5f) == 0x8000);
assert(sc_float_to_u16fp(0.75f) == 0xc000);
assert(sc_float_to_u16fp(1.0f) == 0xffff);
}
static void test_float_to_i16fp(void) {
assert(sc_float_to_i16fp(0.0f) == 0);
assert(sc_float_to_i16fp(0.03125f) == 0x400);
assert(sc_float_to_i16fp(0.0625f) == 0x800);
assert(sc_float_to_i16fp(0.125f) == 0x1000);
assert(sc_float_to_i16fp(0.25f) == 0x2000);
assert(sc_float_to_i16fp(0.5f) == 0x4000);
assert(sc_float_to_i16fp(0.75f) == 0x6000);
assert(sc_float_to_i16fp(1.0f) == 0x7fff);
assert(sc_float_to_i16fp(-0.03125f) == -0x400);
assert(sc_float_to_i16fp(-0.0625f) == -0x800);
assert(sc_float_to_i16fp(-0.125f) == -0x1000);
assert(sc_float_to_i16fp(-0.25f) == -0x2000);
assert(sc_float_to_i16fp(-0.5f) == -0x4000);
assert(sc_float_to_i16fp(-0.75f) == -0x6000);
assert(sc_float_to_i16fp(-1.0f) == -0x8000);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_write16be();
test_write32be();
test_write64be();
test_read16be();
test_read32be();
test_read64be();
test_float_to_u16fp();
test_float_to_i16fp();
return 0;
}

View File

@ -1,81 +0,0 @@
#include "common.h"
#include <assert.h>
#include "util/buffer_util.h"
static void test_buffer_write16be(void) {
uint16_t val = 0xABCD;
uint8_t buf[2];
sc_write16be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
}
static void test_buffer_write32be(void) {
uint32_t val = 0xABCD1234;
uint8_t buf[4];
sc_write32be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
}
static void test_buffer_write64be(void) {
uint64_t val = 0xABCD1234567890EF;
uint8_t buf[8];
sc_write64be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
assert(buf[4] == 0x56);
assert(buf[5] == 0x78);
assert(buf[6] == 0x90);
assert(buf[7] == 0xEF);
}
static void test_buffer_read16be(void) {
uint8_t buf[2] = {0xAB, 0xCD};
uint16_t val = sc_read16be(buf);
assert(val == 0xABCD);
}
static void test_buffer_read32be(void) {
uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34};
uint32_t val = sc_read32be(buf);
assert(val == 0xABCD1234);
}
static void test_buffer_read64be(void) {
uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34,
0x56, 0x78, 0x90, 0xEF};
uint64_t val = sc_read64be(buf);
assert(val == 0xABCD1234567890EF);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_buffer_write16be();
test_buffer_write32be();
test_buffer_write64be();
test_buffer_read16be();
test_buffer_read32be();
test_buffer_read64be();
return 0;
}

View File

@ -132,14 +132,14 @@ static void test_serialize_inject_scroll_event(void) {
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 25); assert(size == 21);
const unsigned char expected[] = { const unsigned char expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920 0x04, 0x38, 0x07, 0x80, // 1080 1920
0x00, 0x00, 0x00, 0x01, // 1 0x7F, 0xFF, // 1 (float encoded as i16)
0xFF, 0xFF, 0xFF, 0xFF, // -1 0x80, 0x00, // -1 (float encoded as i16)
0x00, 0x00, 0x00, 0x01, // 1 0x00, 0x00, 0x00, 0x01, // 1
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));

View File

@ -4,10 +4,10 @@ buildscript {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.3' classpath 'com.android.tools.build:gradle:7.2.2'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
@ -17,7 +17,7 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
tasks.withType(JavaCompile) { tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:deprecation" options.compilerArgs << "-Xlint:deprecation"

View File

@ -20,6 +20,6 @@ ffmpeg_avcodec = 'avcodec-58'
ffmpeg_avformat = 'avformat-58' ffmpeg_avformat = 'avformat-58'
ffmpeg_avutil = 'avutil-56' ffmpeg_avutil = 'avutil-56'
prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1' prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1'
prebuilt_sdl2 = 'SDL2-2.0.22/i686-w64-mingw32' prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32'
prebuilt_libusb_root = 'libusb-1.0.26' prebuilt_libusb_root = 'libusb-1.0.26'
prebuilt_libusb = prebuilt_libusb_root + '/MinGW-Win32' prebuilt_libusb = 'libusb-1.0.26/MinGW-Win32'

View File

@ -19,7 +19,7 @@ endian = 'little'
ffmpeg_avcodec = 'avcodec-59' ffmpeg_avcodec = 'avcodec-59'
ffmpeg_avformat = 'avformat-59' ffmpeg_avformat = 'avformat-59'
ffmpeg_avutil = 'avutil-57' ffmpeg_avutil = 'avutil-57'
prebuilt_ffmpeg = 'ffmpeg-win64-5.0.1' prebuilt_ffmpeg = 'ffmpeg-win64-5.1.2'
prebuilt_sdl2 = 'SDL2-2.0.22/x86_64-w64-mingw32' prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32'
prebuilt_libusb_root = 'libusb-1.0.26' prebuilt_libusb_root = 'libusb-1.0.26'
prebuilt_libusb = prebuilt_libusb_root + '/MinGW-x64' prebuilt_libusb = 'libusb-1.0.26/MinGW-x64'

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -2,8 +2,8 @@
set -e set -e
BUILDDIR=build-auto BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24 PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25
PREBUILT_SERVER_SHA256=ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056 PREBUILT_SERVER_SHA256=ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb
echo "[scrcpy] Downloading prebuilt server..." echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server wget "$PREBUILT_SERVER_URL" -O scrcpy-server
@ -12,7 +12,7 @@ echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check
echo "[scrcpy] Building client..." echo "[scrcpy] Building client..."
rm -rf "$BUILDDIR" rm -rf "$BUILDDIR"
meson "$BUILDDIR" --buildtype=release --strip -Db_lto=true \ meson setup "$BUILDDIR" --buildtype=release --strip -Db_lto=true \
-Dprebuilt_server=scrcpy-server -Dprebuilt_server=scrcpy-server
cd "$BUILDDIR" cd "$BUILDDIR"
ninja ninja

View File

@ -1,5 +1,5 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '1.24', version: '1.25',
meson_version: '>= 0.48', meson_version: '>= 0.48',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',

View File

@ -24,13 +24,13 @@ SERVER_BUILD_DIR := build-server
WIN32_BUILD_DIR := build-win32 WIN32_BUILD_DIR := build-win32
WIN64_BUILD_DIR := build-win64 WIN64_BUILD_DIR := build-win64
DIST := dist
WIN32_TARGET_DIR := scrcpy-win32
WIN64_TARGET_DIR := scrcpy-win64
VERSION := $(shell git describe --tags --always) VERSION := $(shell git describe --tags --always)
WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip
WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip DIST := dist
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)
WIN64_TARGET_DIR := scrcpy-win64-$(VERSION)
WIN32_TARGET := $(WIN32_TARGET_DIR).zip
WIN64_TARGET := $(WIN64_TARGET_DIR).zip
RELEASE_DIR := release-$(VERSION) RELEASE_DIR := release-$(VERSION)
@ -53,13 +53,13 @@ clean:
test: test:
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \ [ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
meson "$(TEST_BUILD_DIR)" -Db_sanitize=address ) meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address )
ninja -C "$(TEST_BUILD_DIR)" ninja -C "$(TEST_BUILD_DIR)"
$(GRADLE) -p server check $(GRADLE) -p server check
build-server: build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)" ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32: prepare-deps-win32:
@ -76,7 +76,7 @@ prepare-deps-win64:
build-win32: prepare-deps-win32 build-win32: prepare-deps-win32
[ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \
meson "$(WIN32_BUILD_DIR)" \ meson setup "$(WIN32_BUILD_DIR)" \
--cross-file cross_win32.txt \ --cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcompile_server=false \ -Dcompile_server=false \
@ -85,7 +85,7 @@ build-win32: prepare-deps-win32
build-win64: prepare-deps-win64 build-win64: prepare-deps-win64
[ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \
meson "$(WIN64_BUILD_DIR)" \ meson setup "$(WIN64_BUILD_DIR)" \
--cross-file cross_win64.txt \ --cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcompile_server=false \ -Dcompile_server=false \
@ -105,10 +105,10 @@ dist-win32: build-server build-win32
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/SDL2-2.0.22/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.26.1/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 dist-win64: build-server build-win64
@ -119,21 +119,21 @@ dist-win64: build-server build-win64
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/SDL2-2.0.22/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.26.1/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32 zip-win32: dist-win32
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ cd "$(DIST)"; \
zip -r "../$(WIN32_TARGET)" . zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
zip-win64: dist-win64 zip-win64: dist-win64
cd "$(DIST)/$(WIN64_TARGET_DIR)"; \ cd "$(DIST)"; \
zip -r "../$(WIN64_TARGET)" . zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"

View File

@ -1,13 +1,13 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 31 compileSdkVersion 33
defaultConfig { defaultConfig {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 31 targetSdkVersion 33
versionCode 12400 versionCode 12500
versionName "1.24" versionName "1.25"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
@ -19,8 +19,7 @@ android {
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) testImplementation 'junit:junit:4.13.2'
testImplementation 'junit:junit:4.13.1'
} }
apply from: "$project.rootDir/config/android-checkstyle.gradle" apply from: "$project.rootDir/config/android-checkstyle.gradle"

View File

@ -12,25 +12,29 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.24 SCRCPY_VERSION_NAME=1.25
PLATFORM=${ANDROID_PLATFORM:-31} PLATFORM=${ANDROID_PLATFORM:-33}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0}
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
CLASSES_DIR="$BUILD_DIR/classes" CLASSES_DIR="$BUILD_DIR/classes"
GEN_DIR="$BUILD_DIR/gen"
SERVER_DIR=$(dirname "$0") SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server SERVER_BINARY=scrcpy-server
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar"
echo "Platform: android-$PLATFORM" echo "Platform: android-$PLATFORM"
echo "Build-tools: $BUILD_TOOLS" echo "Build-tools: $BUILD_TOOLS"
echo "Build dir: $BUILD_DIR" echo "Build dir: $BUILD_DIR"
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex rm -rf "$CLASSES_DIR" "$GEN_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" mkdir -p "$CLASSES_DIR"
mkdir -p "$GEN_DIR/com/genymobile/scrcpy"
<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java" << EOF cat > "$GEN_DIR/com/genymobile/scrcpy/BuildConfig.java"
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
public final class BuildConfig { public final class BuildConfig {
@ -41,14 +45,15 @@ EOF
echo "Generating java from aidl..." echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl" cd "$SERVER_DIR/src/main/aidl"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
android/view/IRotationWatcher.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
android/content/IOnPrimaryClipChangedListener.aidl android/content/IOnPrimaryClipChangedListener.aidl
echo "Compiling java sources..." echo "Compiling java sources..."
cd ../java cd ../java
javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \ javac -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \
-d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \ -source 1.8 -target 1.8 \
com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/wrappers/*.java com/genymobile/scrcpy/wrappers/*.java
@ -59,8 +64,7 @@ cd "$CLASSES_DIR"
if [[ $PLATFORM -lt 31 ]] if [[ $PLATFORM -lt 31 ]]
then then
# use dx # use dx
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ "$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \
--output "$BUILD_DIR/classes.dex" \
android/view/*.class \ android/view/*.class \
android/content/*.class \ android/content/*.class \
com/genymobile/scrcpy/*.class \ com/genymobile/scrcpy/*.class \
@ -69,10 +73,10 @@ then
echo "Archiving..." echo "Archiving..."
cd "$BUILD_DIR" cd "$BUILD_DIR"
jar cvf "$SERVER_BINARY" classes.dex jar cvf "$SERVER_BINARY" classes.dex
rm -rf classes.dex classes rm -rf classes.dex
else else
# use d8 # use d8
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \ "$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \
--output "$BUILD_DIR/classes.zip" \ --output "$BUILD_DIR/classes.zip" \
android/view/*.class \ android/view/*.class \
android/content/*.class \ android/content/*.class \
@ -81,7 +85,8 @@ else
cd "$BUILD_DIR" cd "$BUILD_DIR"
mv classes.zip "$SERVER_BINARY" mv classes.zip "$SERVER_BINARY"
rm -rf classes
fi fi
rm -rf "$GEN_DIR" "$CLASSES_DIR"
echo "Server generated in $BUILD_DIR/$SERVER_BINARY" echo "Server generated in $BUILD_DIR/$SERVER_BINARY"

View File

@ -13,8 +13,8 @@ if prebuilt_server == ''
install_dir: 'share/scrcpy') install_dir: 'share/scrcpy')
else else
if not prebuilt_server.startswith('/') if not prebuilt_server.startswith('/')
# relative path needs some trick # prebuilt server path is relative to the root scrcpy directory
prebuilt_server = meson.source_root() + '/' + prebuilt_server prebuilt_server = '../' + prebuilt_server
endif endif
custom_target('scrcpy-server-prebuilt', custom_target('scrcpy-server-prebuilt',
input: prebuilt_server, input: prebuilt_server,

View File

@ -0,0 +1,38 @@
package com.genymobile.scrcpy;
public final class Binary {
private Binary() {
// not instantiable
}
public static int toUnsigned(short value) {
return value & 0xffff;
}
public static int toUnsigned(byte value) {
return value & 0xff;
}
/**
* Convert unsigned 16-bit fixed-point to a float between 0 and 1
*
* @param value encoded value
* @return Float value between 0 and 1
*/
public static float u16FixedPointToFloat(short value) {
int unsignedShort = Binary.toUnsigned(value);
// 0x1p16f is 2^16 as float
return unsignedShort == 0xffff ? 1f : (unsignedShort / 0x1p16f);
}
/**
* Convert signed 16-bit fixed-point to a float between -1 and 1
*
* @param value encoded value
* @return Float value between -1 and 1
*/
public static float i16FixedPointToFloat(short value) {
// 0x1p15f is 2^15 as float
return value == 0x7fff ? 1f : (value / 0x1p15f);
}
}

View File

@ -1,7 +1,5 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.util.Base64; import android.util.Base64;
@ -164,12 +162,10 @@ public final class CleanUp {
Config config = Config.fromBase64(args[0]); Config config = Config.fromBase64(args[0]);
if (config.disableShowTouches || config.restoreStayOn != -1) { if (config.disableShowTouches || config.restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager();
Settings settings = new Settings(serviceManager);
if (config.disableShowTouches) { if (config.disableShowTouches) {
Ln.i("Disabling \"show touches\""); Ln.i("Disabling \"show touches\"");
try { try {
settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
} catch (SettingsException e) { } catch (SettingsException e) {
Ln.e("Could not restore \"show_touches\"", e); Ln.e("Could not restore \"show_touches\"", e);
} }
@ -177,7 +173,7 @@ public final class CleanUp {
if (config.restoreStayOn != -1) { if (config.restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\""); Ln.i("Restoring \"stay awake\"");
try { try {
settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
} catch (SettingsException e) { } catch (SettingsException e) {
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
} }

View File

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

View File

@ -30,4 +30,14 @@ public final class Command {
} }
return result; return result;
} }
public static String execReadOutput(String... cmd) throws IOException, InterruptedException {
Process process = Runtime.getRuntime().exec(cmd);
String output = IO.toString(process.getInputStream());
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
}
return output;
}
} }

View File

@ -33,8 +33,8 @@ public final class ControlMessage {
private long pointerId; private long pointerId;
private float pressure; private float pressure;
private Position position; private Position position;
private int hScroll; private float hScroll;
private int vScroll; private float vScroll;
private int copyKey; private int copyKey;
private boolean paste; private boolean paste;
private int repeat; private int repeat;
@ -71,7 +71,7 @@ public final class ControlMessage {
return msg; return msg;
} }
public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll, int buttons) { public static ControlMessage createInjectScrollEvent(Position position, float hScroll, float vScroll, int buttons) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_SCROLL_EVENT; msg.type = TYPE_INJECT_SCROLL_EVENT;
msg.position = position; msg.position = position;
@ -156,11 +156,11 @@ public final class ControlMessage {
return position; return position;
} }
public int getHScroll() { public float getHScroll() {
return hScroll; return hScroll;
} }
public int getVScroll() { public float getVScroll() {
return vScroll; return vScroll;
} }

View File

@ -10,7 +10,7 @@ public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 24; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int BACK_OR_SCREEN_ON_LENGTH = 1; static final int BACK_OR_SCREEN_ON_LENGTH = 1;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
static final int GET_CLIPBOARD_LENGTH = 1; static final int GET_CLIPBOARD_LENGTH = 1;
@ -103,7 +103,7 @@ public class ControlMessageReader {
if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) { if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) {
return null; return null;
} }
int action = toUnsigned(buffer.get()); int action = Binary.toUnsigned(buffer.get());
int keycode = buffer.getInt(); int keycode = buffer.getInt();
int repeat = buffer.getInt(); int repeat = buffer.getInt();
int metaState = buffer.getInt(); int metaState = buffer.getInt();
@ -136,13 +136,10 @@ public class ControlMessageReader {
if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) {
return null; return null;
} }
int action = toUnsigned(buffer.get()); int action = Binary.toUnsigned(buffer.get());
long pointerId = buffer.getLong(); long pointerId = buffer.getLong();
Position position = readPosition(buffer); Position position = readPosition(buffer);
// 16 bits fixed-point float pressure = Binary.u16FixedPointToFloat(buffer.getShort());
int pressureInt = toUnsigned(buffer.getShort());
// convert it to a float between 0 and 1 (0x1p16f is 2^16 as float)
float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f);
int buttons = buffer.getInt(); int buttons = buffer.getInt();
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons); return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
} }
@ -152,8 +149,8 @@ public class ControlMessageReader {
return null; return null;
} }
Position position = readPosition(buffer); Position position = readPosition(buffer);
int hScroll = buffer.getInt(); float hScroll = Binary.i16FixedPointToFloat(buffer.getShort());
int vScroll = buffer.getInt(); float vScroll = Binary.i16FixedPointToFloat(buffer.getShort());
int buttons = buffer.getInt(); int buttons = buffer.getInt();
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons);
} }
@ -162,7 +159,7 @@ public class ControlMessageReader {
if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) { if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) {
return null; return null;
} }
int action = toUnsigned(buffer.get()); int action = Binary.toUnsigned(buffer.get());
return ControlMessage.createBackOrScreenOn(action); return ControlMessage.createBackOrScreenOn(action);
} }
@ -170,7 +167,7 @@ public class ControlMessageReader {
if (buffer.remaining() < GET_CLIPBOARD_LENGTH) { if (buffer.remaining() < GET_CLIPBOARD_LENGTH) {
return null; return null;
} }
int copyKey = toUnsigned(buffer.get()); int copyKey = Binary.toUnsigned(buffer.get());
return ControlMessage.createGetClipboard(copyKey); return ControlMessage.createGetClipboard(copyKey);
} }
@ -198,16 +195,8 @@ public class ControlMessageReader {
private static Position readPosition(ByteBuffer buffer) { private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt(); int x = buffer.getInt();
int y = buffer.getInt(); int y = buffer.getInt();
int screenWidth = toUnsigned(buffer.getShort()); int screenWidth = Binary.toUnsigned(buffer.getShort());
int screenHeight = toUnsigned(buffer.getShort()); int screenHeight = Binary.toUnsigned(buffer.getShort());
return new Position(x, y, screenWidth, screenHeight); return new Position(x, y, screenWidth, screenHeight);
} }
private static int toUnsigned(short value) {
return value & 0xffff;
}
private static int toUnsigned(byte value) {
return value & 0xff;
}
} }

View File

@ -16,6 +16,10 @@ public class Controller {
private static final int DEFAULT_DEVICE_ID = 0; private static final int DEFAULT_DEVICE_ID = 0;
// control_msg.h values of the pointerId field in inject_touch_event message
private static final int POINTER_ID_MOUSE = -1;
private static final int POINTER_ID_VIRTUAL_MOUSE = -3;
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private final Device device; private final Device device;
@ -71,7 +75,7 @@ public class Controller {
SystemClock.sleep(500); SystemClock.sleep(500);
} }
while (true) { while (!Thread.currentThread().isInterrupted()) {
handleEvent(); handleEvent();
} }
} }
@ -194,7 +198,19 @@ public class Controller {
pointer.setPressure(pressure); pointer.setPressure(pressure);
pointer.setUp(action == MotionEvent.ACTION_UP); pointer.setUp(action == MotionEvent.ACTION_UP);
int source;
int pointerCount = pointersState.update(pointerProperties, pointerCoords); int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
// real mouse event (forced by the client when --forward-on-click)
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
source = InputDevice.SOURCE_MOUSE;
} else {
// POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER;
source = InputDevice.SOURCE_TOUCHSCREEN;
// Buttons must not be set for touch events
buttons = 0;
}
if (pointerCount == 1) { if (pointerCount == 1) {
if (action == MotionEvent.ACTION_DOWN) { if (action == MotionEvent.ACTION_DOWN) {
@ -209,21 +225,13 @@ public class Controller {
} }
} }
// Right-click and middle-click only work if the source is a mouse
boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0;
int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN;
if (source != InputDevice.SOURCE_MOUSE) {
// Buttons must not be set for touch events
buttons = 0;
}
MotionEvent event = MotionEvent MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
0); 0);
return device.injectEvent(event, Device.INJECT_MODE_ASYNC); return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
} }
private boolean injectScroll(Position position, int hScroll, int vScroll, int buttons) { private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) {
long now = SystemClock.uptimeMillis(); long now = SystemClock.uptimeMillis();
Point point = device.getPhysicalPoint(position); Point point = device.getPhysicalPoint(position);
if (point == null) { if (point == null) {
@ -250,12 +258,9 @@ public class Controller {
* Schedule a call to set power mode to off after a small delay. * Schedule a call to set power mode to off after a small delay.
*/ */
private static void schedulePowerModeOff() { private static void schedulePowerModeOff() {
EXECUTOR.schedule(new Runnable() { EXECUTOR.schedule(() -> {
@Override Ln.i("Forcing screen off");
public void run() { Device.setScreenPowerMode(Device.POWER_MODE_OFF);
Ln.i("Forcing screen off");
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
}
}, 200, TimeUnit.MILLISECONDS); }, 200, TimeUnit.MILLISECONDS);
} }

View File

@ -15,7 +15,7 @@ public final class DesktopConnection implements Closeable {
private static final int DEVICE_NAME_FIELD_LENGTH = 64; private static final int DEVICE_NAME_FIELD_LENGTH = 64;
private static final String SOCKET_NAME = "scrcpy"; private static final String SOCKET_NAME_PREFIX = "scrcpy";
private final LocalSocket videoSocket; private final LocalSocket videoSocket;
private final FileDescriptor videoFd; private final FileDescriptor videoFd;
@ -46,12 +46,22 @@ public final class DesktopConnection implements Closeable {
return localSocket; return localSocket;
} }
public static DesktopConnection open(boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException { private static String getSocketName(int uid) {
if (uid == -1) {
// If no UID is set, use "scrcpy" to simplify using scrcpy-server alone
return SOCKET_NAME_PREFIX;
}
return SOCKET_NAME_PREFIX + String.format("_%08x", uid);
}
public static DesktopConnection open(int uid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
String socketName = getSocketName(uid);
LocalSocket videoSocket; LocalSocket videoSocket;
LocalSocket controlSocket = null; LocalSocket controlSocket = null;
if (tunnelForward) { if (tunnelForward) {
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME); try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
try {
videoSocket = localServerSocket.accept(); videoSocket = localServerSocket.accept();
if (sendDummyByte) { if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error // send one byte so the client may read() to detect a connection error
@ -65,14 +75,12 @@ public final class DesktopConnection implements Closeable {
throw e; throw e;
} }
} }
} finally {
localServerSocket.close();
} }
} else { } else {
videoSocket = connect(SOCKET_NAME); videoSocket = connect(socketName);
if (control) { if (control) {
try { try {
controlSocket = connect(SOCKET_NAME); controlSocket = connect(socketName);
} catch (IOException | RuntimeException e) { } catch (IOException | RuntimeException e) {
videoSocket.close(); videoSocket.close();
throw e; throw e;

View File

@ -31,9 +31,6 @@ public final class Device {
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; 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 { public interface RotationListener {
void onRotationChanged(int rotation); void onRotationChanged(int rotation);
} }
@ -66,9 +63,9 @@ public final class Device {
public Device(Options options) { public Device(Options options) {
displayId = options.getDisplayId(); displayId = options.getDisplayId();
DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId); DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) { if (displayInfo == null) {
int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds(); int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds();
throw new InvalidDisplayIdException(displayId, displayIds); throw new InvalidDisplayIdException(displayId, displayIds);
} }
@ -82,7 +79,7 @@ public final class Device {
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
layerStack = displayInfo.getLayerStack(); layerStack = displayInfo.getLayerStack();
SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { ServiceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
@Override @Override
public void onRotationChanged(int rotation) { public void onRotationChanged(int rotation) {
synchronized (Device.this) { synchronized (Device.this) {
@ -98,7 +95,7 @@ public final class Device {
if (options.getControl() && options.getClipboardAutosync()) { if (options.getControl() && options.getClipboardAutosync()) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically // If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardManager != null) { if (clipboardManager != null) {
clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
@Override @Override
@ -192,7 +189,7 @@ public final class Device {
return false; return false;
} }
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, injectMode); return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode);
} }
public boolean injectEvent(InputEvent event, int injectMode) { public boolean injectEvent(InputEvent event, int injectMode) {
@ -220,7 +217,7 @@ public final class Device {
} }
public static boolean isScreenOn() { public static boolean isScreenOn() {
return SERVICE_MANAGER.getPowerManager().isScreenOn(); return ServiceManager.getPowerManager().isScreenOn();
} }
public synchronized void setRotationListener(RotationListener rotationListener) { public synchronized void setRotationListener(RotationListener rotationListener) {
@ -232,19 +229,19 @@ public final class Device {
} }
public static void expandNotificationPanel() { public static void expandNotificationPanel() {
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel(); ServiceManager.getStatusBarManager().expandNotificationsPanel();
} }
public static void expandSettingsPanel() { public static void expandSettingsPanel() {
SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel(); ServiceManager.getStatusBarManager().expandSettingsPanel();
} }
public static void collapsePanels() { public static void collapsePanels() {
SERVICE_MANAGER.getStatusBarManager().collapsePanels(); ServiceManager.getStatusBarManager().collapsePanels();
} }
public static String getClipboardText() { public static String getClipboardText() {
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardManager == null) { if (clipboardManager == null) {
return null; return null;
} }
@ -256,7 +253,7 @@ public final class Device {
} }
public boolean setClipboardText(String text) { public boolean setClipboardText(String text) {
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardManager == null) { if (clipboardManager == null) {
return false; return false;
} }
@ -299,7 +296,7 @@ public final class Device {
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
*/ */
public static void rotateDevice() { public static void rotateDevice() {
WindowManager wm = SERVICE_MANAGER.getWindowManager(); WindowManager wm = ServiceManager.getWindowManager();
boolean accelerometerRotation = !wm.isRotationFrozen(); boolean accelerometerRotation = !wm.isRotationFrozen();
@ -315,8 +312,4 @@ public final class Device {
wm.thawRotation(); wm.thawRotation();
} }
} }
public static Settings getSettings() {
return SETTINGS;
}
} }

View File

@ -25,7 +25,7 @@ public final class DeviceMessageSender {
} }
public void loop() throws IOException, InterruptedException { public void loop() throws IOException, InterruptedException {
while (true) { while (!Thread.currentThread().isInterrupted()) {
String text; String text;
long sequence; long sequence;
synchronized (this) { synchronized (this) {

View File

@ -6,7 +6,9 @@ import android.system.OsConstants;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Scanner;
public final class IO { public final class IO {
private IO() { private IO() {
@ -37,4 +39,13 @@ public final class IO {
public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException { public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException {
writeFully(fd, ByteBuffer.wrap(buffer, offset, len)); writeFully(fd, ByteBuffer.wrap(buffer, offset, len));
} }
public static String toString(InputStream inputStream) {
StringBuilder builder = new StringBuilder();
Scanner scanner = new Scanner(inputStream);
while (scanner.hasNextLine()) {
builder.append(scanner.nextLine()).append('\n');
}
return builder.toString();
}
} }

View File

@ -6,6 +6,7 @@ import java.util.List;
public class Options { public class Options {
private Ln.Level logLevel = Ln.Level.DEBUG; private Ln.Level logLevel = Ln.Level.DEBUG;
private int uid = -1; // 31-bit non-negative value, or -1
private int maxSize; private int maxSize;
private int bitRate = 8000000; private int bitRate = 8000000;
private int maxFps; private int maxFps;
@ -37,6 +38,14 @@ public class Options {
this.logLevel = logLevel; this.logLevel = logLevel;
} }
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
} }

View File

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

View File

@ -9,6 +9,7 @@ import android.media.MediaCodecList;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock;
import android.view.Surface; import android.view.Surface;
import java.io.FileDescriptor; import java.io.FileDescriptor;
@ -75,63 +76,88 @@ public class ScreenEncoder implements Device.RotationListener {
} }
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaCodec codec = createCodec(encoderName);
MediaFormat format = createFormat(bitRate, maxFps, codecOptions); MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
IBinder display = createDisplay();
device.setRotationListener(this); device.setRotationListener(this);
boolean alive; boolean alive;
try { try {
do { do {
MediaCodec codec = createCodec(encoderName);
IBinder display = createDisplay();
ScreenInfo screenInfo = device.getScreenInfo(); ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect(); Rect contentRect = screenInfo.getContentRect();
// include the locked video orientation // include the locked video orientation
Rect videoRect = screenInfo.getVideoSize().toRect(); Rect videoRect = screenInfo.getVideoSize().toRect();
// does not include the locked video orientation format.setInteger(MediaFormat.KEY_WIDTH, videoRect.width());
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height());
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
setSize(format, videoRect.width(), videoRect.height());
Surface surface = null; Surface surface = null;
try { try {
configure(codec, format); codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = codec.createInputSurface(); surface = codec.createInputSurface();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
codec.start(); codec.start();
alive = encode(codec, fd); alive = encode(codec, fd);
// do not call stop() on exception, it would trigger an IllegalStateException // do not call stop() on exception, it would trigger an IllegalStateException
codec.stop(); codec.stop();
} catch (MediaCodec.CodecException e) {
Ln.e("Codec error: " + e.getMessage());
// <https://developer.android.com/reference/android/media/MediaCodec#error-handling>
// For simplicity, handle isTransient() like isRecoverable()
if (e.isRecoverable() || e.isTransient()) {
// Avoid busy-loop if too many errors are generated
SystemClock.sleep(50);
} else if (!prepareDownsizeRetry(device, screenInfo)) {
throw e;
}
alive = true;
} catch (IllegalStateException | IllegalArgumentException e) { } catch (IllegalStateException | IllegalArgumentException e) {
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
if (!downsizeOnError || firstFrameSent) { if (!prepareDownsizeRetry(device, screenInfo)) {
// Fail immediately
throw e; throw e;
} }
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
if (newMaxSize == 0) {
// Definitively fail
throw e;
}
// Retry with a smaller device size
Ln.i("Retrying with -m" + newMaxSize + "...");
device.setMaxSize(newMaxSize);
alive = true; alive = true;
} finally { } finally {
destroyDisplay(display); codec.reset();
codec.release();
if (surface != null) { if (surface != null) {
surface.release(); surface.release();
} }
} }
} while (alive); } while (alive);
} finally { } finally {
codec.release();
device.setRotationListener(null); device.setRotationListener(null);
SurfaceControl.destroyDisplay(display);
} }
} }
private boolean prepareDownsizeRetry(Device device, ScreenInfo screenInfo) {
if (!downsizeOnError || firstFrameSent) {
Ln.i("#1 " + downsizeOnError + " " + firstFrameSent);
// Must fail immediately
return false;
}
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
Ln.i("newMaxSize = " + newMaxSize);
if (newMaxSize == 0) {
// Must definitively fail
return false;
}
// Retry with a smaller device size
Ln.i("Retrying with -m" + newMaxSize + "...");
device.setMaxSize(newMaxSize);
return true;
}
private static int chooseMaxSizeFallback(Size failedSize) { private static int chooseMaxSizeFallback(Size failedSize) {
int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight()); int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight());
for (int value : MAX_SIZE_FALLBACK) { for (int value : MAX_SIZE_FALLBACK) {
@ -278,15 +304,6 @@ public class ScreenEncoder implements Device.RotationListener {
return SurfaceControl.createDisplay("scrcpy", secure); return SurfaceControl.createDisplay("scrcpy", secure);
} }
private static void configure(MediaCodec codec, MediaFormat format) {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}
private static void setSize(MediaFormat format, int width, int height) {
format.setInteger(MediaFormat.KEY_WIDTH, width);
format.setInteger(MediaFormat.KEY_HEIGHT, height);
}
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
SurfaceControl.openTransaction(); SurfaceControl.openTransaction();
try { try {
@ -297,8 +314,4 @@ public class ScreenEncoder implements Device.RotationListener {
SurfaceControl.closeTransaction(); SurfaceControl.closeTransaction();
} }
} }
private static void destroyDisplay(IBinder display) {
SurfaceControl.destroyDisplay(display);
}
} }

View File

@ -20,10 +20,9 @@ public final class Server {
int restoreStayOn = -1; int restoreStayOn = -1;
boolean restoreNormalPowerMode = options.getControl(); // only restore power mode if control is enabled boolean restoreNormalPowerMode = options.getControl(); // only restore power mode if control is enabled
if (options.getShowTouches() || options.getStayAwake()) { if (options.getShowTouches() || options.getStayAwake()) {
Settings settings = Device.getSettings();
if (options.getShowTouches()) { if (options.getShowTouches()) {
try { try {
String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up // If "show touches" was disabled, it must be disabled back on clean up
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
} catch (SettingsException e) { } catch (SettingsException e) {
@ -34,7 +33,7 @@ public final class Server {
if (options.getStayAwake()) { if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
try { try {
String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
try { try {
restoreStayOn = Integer.parseInt(oldValue); restoreStayOn = Integer.parseInt(oldValue);
if (restoreStayOn == stayOn) { if (restoreStayOn == stayOn) {
@ -67,11 +66,12 @@ public final class Server {
Thread initThread = startInitThread(options); Thread initThread = startInitThread(options);
int uid = options.getUid();
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
boolean control = options.getControl(); boolean control = options.getControl();
boolean sendDummyByte = options.getSendDummyByte(); boolean sendDummyByte = options.getSendDummyByte();
try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control, sendDummyByte)) { try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
if (options.getSendDeviceMeta()) { if (options.getSendDeviceMeta()) {
Size videoSize = device.getScreenInfo().getVideoSize(); Size videoSize = device.getScreenInfo().getVideoSize();
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
@ -88,12 +88,7 @@ public final class Server {
controllerThread = startController(controller); controllerThread = startController(controller);
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender()); deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
device.setClipboardListener(new Device.ClipboardListener() { device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
@Override
public void onClipboardTextChanged(String text) {
controller.getSender().pushClipboardText(text);
}
});
} }
try { try {
@ -115,26 +110,18 @@ public final class Server {
} }
private static Thread startInitThread(final Options options) { private static Thread startInitThread(final Options options) {
Thread thread = new Thread(new Runnable() { Thread thread = new Thread(() -> initAndCleanUp(options));
@Override
public void run() {
initAndCleanUp(options);
}
});
thread.start(); thread.start();
return thread; return thread;
} }
private static Thread startController(final Controller controller) { private static Thread startController(final Controller controller) {
Thread thread = new Thread(new Runnable() { Thread thread = new Thread(() -> {
@Override try {
public void run() { controller.control();
try { } catch (IOException e) {
controller.control(); // this is expected on close
} catch (IOException e) { Ln.d("Controller stopped");
// this is expected on close
Ln.d("Controller stopped");
}
} }
}); });
thread.start(); thread.start();
@ -142,15 +129,12 @@ public final class Server {
} }
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) { private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
Thread thread = new Thread(new Runnable() { Thread thread = new Thread(() -> {
@Override try {
public void run() { sender.loop();
try { } catch (IOException | InterruptedException e) {
sender.loop(); // this is expected on close
} catch (IOException | InterruptedException e) { Ln.d("Device message sender stopped");
// this is expected on close
Ln.d("Device message sender stopped");
}
} }
}); });
thread.start(); thread.start();
@ -179,6 +163,13 @@ public final class Server {
String key = arg.substring(0, equalIndex); String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1); String value = arg.substring(equalIndex + 1);
switch (key) { switch (key) {
case "uid":
int uid = Integer.parseInt(value, 0x10);
if (uid < -1) {
throw new IllegalArgumentException("uid may not be negative (except -1 for 'none'): " + uid);
}
options.setUid(uid);
break;
case "log_level": case "log_level":
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
options.setLogLevel(level); options.setLogLevel(level);
@ -320,12 +311,9 @@ public final class Server {
} }
public static void main(String... args) throws Exception { public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
@Override Ln.e("Exception on thread " + t, e);
public void uncaughtException(Thread t, Throwable e) { suggestFix(e);
Ln.e("Exception on thread " + t, e);
suggestFix(e);
}
}); });
Options options = createOptions(args); Options options = createOptions(args);

View File

@ -7,16 +7,14 @@ import android.os.Build;
import java.io.IOException; import java.io.IOException;
public class Settings { public final class Settings {
public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM; public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM;
public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE; public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE;
public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL; public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL;
private final ServiceManager serviceManager; private Settings() {
/* not instantiable */
public Settings(ServiceManager serviceManager) {
this.serviceManager = serviceManager;
} }
private static void execSettingsPut(String table, String key, String value) throws SettingsException { private static void execSettingsPut(String table, String key, String value) throws SettingsException {
@ -35,10 +33,10 @@ public class Settings {
} }
} }
public String getValue(String table, String key) throws SettingsException { public static String getValue(String table, String key) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788> // on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
return provider.getValue(table, key); return provider.getValue(table, key);
} catch (SettingsException e) { } catch (SettingsException e) {
Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e); Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e);
@ -48,10 +46,10 @@ public class Settings {
return execSettingsGet(table, key); return execSettingsGet(table, key);
} }
public void putValue(String table, String key, String value) throws SettingsException { public static void putValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788> // on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
provider.putValue(table, key, value); provider.putValue(table, key, value);
} catch (SettingsException e) { } catch (SettingsException e) {
Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e); Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e);
@ -61,10 +59,10 @@ public class Settings {
execSettingsPut(table, key, value); execSettingsPut(table, key, value);
} }
public String getAndPutValue(String table, String key, String value) throws SettingsException { public static String getAndPutValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788> // on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
String oldValue = provider.getValue(table, key); String oldValue = provider.getValue(table, key);
if (!value.equals(oldValue)) { if (!value.equals(oldValue)) {
provider.putValue(table, key, value); provider.putValue(table, key, value);

View File

@ -15,6 +15,9 @@ public class ClipboardManager {
private Method getPrimaryClipMethod; private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod; private Method setPrimaryClipMethod;
private Method addPrimaryClipChangedListener; private Method addPrimaryClipChangedListener;
private boolean alternativeGetMethod;
private boolean alternativeSetMethod;
private boolean alternativeAddListenerMethod;
public ClipboardManager(IInterface manager) { public ClipboardManager(IInterface manager) {
this.manager = manager; this.manager = manager;
@ -25,7 +28,12 @@ public class ClipboardManager {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
} else { } else {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
} catch (NoSuchMethodException e) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
alternativeGetMethod = true;
}
} }
} }
return getPrimaryClipMethod; return getPrimaryClipMethod;
@ -36,23 +44,34 @@ public class ClipboardManager {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} else { } else {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
} catch (NoSuchMethodException e) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
alternativeSetMethod = true;
}
} }
} }
return setPrimaryClipMethod; return setPrimaryClipMethod;
} }
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException { private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME); return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
} }
if (alternativeMethod) {
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
}
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
} }
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
throws InvocationTargetException, IllegalAccessException { throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME); method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
} else if (alternativeMethod) {
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
} else { } else {
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
} }
@ -61,7 +80,7 @@ public class ClipboardManager {
public CharSequence getText() { public CharSequence getText() {
try { try {
Method method = getGetPrimaryClipMethod(); Method method = getGetPrimaryClipMethod();
ClipData clipData = getPrimaryClip(method, manager); ClipData clipData = getPrimaryClip(method, alternativeGetMethod, manager);
if (clipData == null || clipData.getItemCount() == 0) { if (clipData == null || clipData.getItemCount() == 0) {
return null; return null;
} }
@ -76,7 +95,7 @@ public class ClipboardManager {
try { try {
Method method = getSetPrimaryClipMethod(); Method method = getSetPrimaryClipMethod();
ClipData clipData = ClipData.newPlainText(null, text); ClipData clipData = ClipData.newPlainText(null, text);
setPrimaryClip(method, manager, clipData); setPrimaryClip(method, alternativeSetMethod, manager, clipData);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
@ -84,10 +103,12 @@ public class ClipboardManager {
} }
} }
private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener) private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
throws InvocationTargetException, IllegalAccessException { IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME); method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
} else if (alternativeMethod) {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
} else { } else {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
} }
@ -99,8 +120,14 @@ public class ClipboardManager {
addPrimaryClipChangedListener = manager.getClass() addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
} else { } else {
addPrimaryClipChangedListener = manager.getClass() try {
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
} catch (NoSuchMethodException e) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, int.class);
alternativeAddListenerMethod = true;
}
} }
} }
return addPrimaryClipChangedListener; return addPrimaryClipChangedListener;
@ -109,7 +136,7 @@ public class ClipboardManager {
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
try { try {
Method method = getAddPrimaryClipChangedListener(); Method method = getAddPrimaryClipChangedListener();
addPrimaryClipChangedListener(method, manager, listener); addPrimaryClipChangedListener(method, alternativeAddListenerMethod, manager, listener);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);

View File

@ -1,22 +1,78 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Command;
import com.genymobile.scrcpy.DisplayInfo; import com.genymobile.scrcpy.DisplayInfo;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.Size; import com.genymobile.scrcpy.Size;
import android.os.IInterface; import android.view.Display;
import java.lang.reflect.Field;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class DisplayManager { public final class DisplayManager {
private final IInterface manager; private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
public DisplayManager(IInterface manager) { public DisplayManager(Object manager) {
this.manager = manager; this.manager = manager;
} }
// public to call it from unit tests
public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) {
Pattern regex = Pattern.compile(
"^ mOverrideDisplayInfo=DisplayInfo\\{\".*?, displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, "
+ "rotation ([0-9]+).*?, layerStack ([0-9]+)",
Pattern.MULTILINE);
Matcher m = regex.matcher(dumpsysDisplayOutput);
if (!m.find()) {
return null;
}
int flags = parseDisplayFlags(m.group(1));
int width = Integer.parseInt(m.group(2));
int height = Integer.parseInt(m.group(3));
int rotation = Integer.parseInt(m.group(4));
int layerStack = Integer.parseInt(m.group(5));
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
}
private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
try {
String dumpsysDisplayOutput = Command.execReadOutput("dumpsys", "display");
return parseDisplayInfo(dumpsysDisplayOutput, displayId);
} catch (Exception e) {
Ln.e("Could not get display info from \"dumpsys display\" output", e);
return null;
}
}
private static int parseDisplayFlags(String text) {
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
if (text == null) {
return 0;
}
int flags = 0;
Matcher m = regex.matcher(text);
while (m.find()) {
String flagString = m.group();
try {
Field filed = Display.class.getDeclaredField(flagString);
flags |= filed.getInt(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
// Silently ignore, some flags reported by "dumpsys display" are @TestApi
}
}
return flags;
}
public DisplayInfo getDisplayInfo(int displayId) { public DisplayInfo getDisplayInfo(int displayId) {
try { try {
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
if (displayInfo == null) { if (displayInfo == null) {
return null; // fallback when displayInfo is null
return getDisplayInfoFromDumpsysDisplay(displayId);
} }
Class<?> cls = displayInfo.getClass(); Class<?> cls = displayInfo.getClass();
// width and height already take the rotation into account // width and height already take the rotation into account

View File

@ -13,27 +13,30 @@ public final class ServiceManager {
public static final String PACKAGE_NAME = "com.android.shell"; public static final String PACKAGE_NAME = "com.android.shell";
public static final int USER_ID = 0; public static final int USER_ID = 0;
private final Method getServiceMethod; private static final Method GET_SERVICE_METHOD;
static {
private WindowManager windowManager;
private DisplayManager displayManager;
private InputManager inputManager;
private PowerManager powerManager;
private StatusBarManager statusBarManager;
private ClipboardManager clipboardManager;
private ActivityManager activityManager;
public ServiceManager() {
try { try {
getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
} catch (Exception e) { } catch (Exception e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
private IInterface getService(String service, String type) { private static WindowManager windowManager;
private static DisplayManager displayManager;
private static InputManager inputManager;
private static PowerManager powerManager;
private static StatusBarManager statusBarManager;
private static ClipboardManager clipboardManager;
private static ActivityManager activityManager;
private ServiceManager() {
/* not instantiable */
}
private static IInterface getService(String service, String type) {
try { try {
IBinder binder = (IBinder) getServiceMethod.invoke(null, service); IBinder binder = (IBinder) GET_SERVICE_METHOD.invoke(null, service);
Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class);
return (IInterface) asInterfaceMethod.invoke(null, binder); return (IInterface) asInterfaceMethod.invoke(null, binder);
} catch (Exception e) { } catch (Exception e) {
@ -41,21 +44,28 @@ public final class ServiceManager {
} }
} }
public WindowManager getWindowManager() { public static WindowManager getWindowManager() {
if (windowManager == null) { if (windowManager == null) {
windowManager = new WindowManager(getService("window", "android.view.IWindowManager")); windowManager = new WindowManager(getService("window", "android.view.IWindowManager"));
} }
return windowManager; return windowManager;
} }
public DisplayManager getDisplayManager() { public static DisplayManager getDisplayManager() {
if (displayManager == null) { if (displayManager == null) {
displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager")); try {
Class<?> clazz = Class.forName("android.hardware.display.DisplayManagerGlobal");
Method getInstanceMethod = clazz.getDeclaredMethod("getInstance");
Object dmg = getInstanceMethod.invoke(null);
displayManager = new DisplayManager(dmg);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new AssertionError(e);
}
} }
return displayManager; return displayManager;
} }
public InputManager getInputManager() { public static InputManager getInputManager() {
if (inputManager == null) { if (inputManager == null) {
try { try {
Method getInstanceMethod = android.hardware.input.InputManager.class.getDeclaredMethod("getInstance"); Method getInstanceMethod = android.hardware.input.InputManager.class.getDeclaredMethod("getInstance");
@ -68,21 +78,21 @@ public final class ServiceManager {
return inputManager; return inputManager;
} }
public PowerManager getPowerManager() { public static PowerManager getPowerManager() {
if (powerManager == null) { if (powerManager == null) {
powerManager = new PowerManager(getService("power", "android.os.IPowerManager")); powerManager = new PowerManager(getService("power", "android.os.IPowerManager"));
} }
return powerManager; return powerManager;
} }
public StatusBarManager getStatusBarManager() { public static StatusBarManager getStatusBarManager() {
if (statusBarManager == null) { if (statusBarManager == null) {
statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService")); statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService"));
} }
return statusBarManager; return statusBarManager;
} }
public ClipboardManager getClipboardManager() { public static ClipboardManager getClipboardManager() {
if (clipboardManager == null) { if (clipboardManager == null) {
IInterface clipboard = getService("clipboard", "android.content.IClipboard"); IInterface clipboard = getService("clipboard", "android.content.IClipboard");
if (clipboard == null) { if (clipboard == null) {
@ -96,7 +106,7 @@ public final class ServiceManager {
return clipboardManager; return clipboardManager;
} }
public ActivityManager getActivityManager() { public static ActivityManager getActivityManager() {
if (activityManager == null) { if (activityManager == null) {
try { try {
// On old Android versions, the ActivityManager is not exposed via AIDL, // On old Android versions, the ActivityManager is not exposed via AIDL,

View File

@ -0,0 +1,42 @@
package com.genymobile.scrcpy;
import org.junit.Assert;
import org.junit.Test;
public class BinaryTest {
@Test
public void testU16FixedPointToFloat() {
final float delta = 0.0f; // on these values, there MUST be no rounding error
Assert.assertEquals(0.0f, Binary.u16FixedPointToFloat((short) 0), delta);
Assert.assertEquals(0.03125f, Binary.u16FixedPointToFloat((short) 0x800), delta);
Assert.assertEquals(0.0625f, Binary.u16FixedPointToFloat((short) 0x1000), delta);
Assert.assertEquals(0.125f, Binary.u16FixedPointToFloat((short) 0x2000), delta);
Assert.assertEquals(0.25f, Binary.u16FixedPointToFloat((short) 0x4000), delta);
Assert.assertEquals(0.5f, Binary.u16FixedPointToFloat((short) 0x8000), delta);
Assert.assertEquals(0.75f, Binary.u16FixedPointToFloat((short) 0xc000), delta);
Assert.assertEquals(1.0f, Binary.u16FixedPointToFloat((short) 0xffff), delta);
}
@Test
public void testI16FixedPointToFloat() {
final float delta = 0.0f; // on these values, there MUST be no rounding error
Assert.assertEquals(0.0f, Binary.i16FixedPointToFloat((short) 0), delta);
Assert.assertEquals(0.03125f, Binary.i16FixedPointToFloat((short) 0x400), delta);
Assert.assertEquals(0.0625f, Binary.i16FixedPointToFloat((short) 0x800), delta);
Assert.assertEquals(0.125f, Binary.i16FixedPointToFloat((short) 0x1000), delta);
Assert.assertEquals(0.25f, Binary.i16FixedPointToFloat((short) 0x2000), delta);
Assert.assertEquals(0.5f, Binary.i16FixedPointToFloat((short) 0x4000), delta);
Assert.assertEquals(0.75f, Binary.i16FixedPointToFloat((short) 0x6000), delta);
Assert.assertEquals(1.0f, Binary.i16FixedPointToFloat((short) 0x7fff), delta);
Assert.assertEquals(-0.03125f, Binary.i16FixedPointToFloat((short) -0x400), delta);
Assert.assertEquals(-0.0625f, Binary.i16FixedPointToFloat((short) -0x800), delta);
Assert.assertEquals(-0.125f, Binary.i16FixedPointToFloat((short) -0x1000), delta);
Assert.assertEquals(-0.25f, Binary.i16FixedPointToFloat((short) -0x2000), delta);
Assert.assertEquals(-0.5f, Binary.i16FixedPointToFloat((short) -0x4000), delta);
Assert.assertEquals(-0.75f, Binary.i16FixedPointToFloat((short) -0x6000), delta);
Assert.assertEquals(-1.0f, Binary.i16FixedPointToFloat((short) -0x8000), delta);
}
}

View File

@ -0,0 +1,242 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import android.view.Display;
import org.junit.Assert;
import org.junit.Test;
public class CommandParserTest {
@Test
public void testParseDisplayInfoFromDumpsysDisplay() {
/* @formatter:off */
String partialOutput = "Logical Displays: size=1\n"
+ " Display 0:\n"
+ "mDisplayId=0\n"
+ " mLayerStack=0\n"
+ " mHasContent=true\n"
+ " mDesiredDisplayModeSpecs={baseModeId=2 primaryRefreshRateRange=[90 90] appRequestRefreshRateRange=[90 90]}\n"
+ " mRequestedColorMode=0\n"
+ " mDisplayOffset=(0, 0)\n"
+ " mDisplayScalingDisabled=false\n"
+ " mPrimaryDisplayDevice=Built-in Screen\n"
+ " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, "
+ "real 1440 x 3120, largest app 1440 x 3120, smallest app 1440 x 3120, appVsyncOff 2000000, presDeadline 11111111, mode 2, "
+ "defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, width=1080, "
+ "height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], "
+ "mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, minimalPostProcessingSupported false, rotation 0, state OFF, "
+ "type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 (515.154 x 514.597) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo DeviceProductInfo{name=, manufacturerPnpId=QCM, "
+ "productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, removeMode 0}\n"
+ " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, "
+ "FLAG_TRUSTED, real 1440 x 3120, largest app 3120 x 2983, smallest app 1440 x 1303, appVsyncOff 2000000, presDeadline 11111111, "
+ "mode 2, defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, "
+ "width=1080, height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities "
+ "HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, "
+ "minimalPostProcessingSupported false, rotation 0, state ON, type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 "
+ "(515.154 x 514.597) dpi, layerStack 0, colorMode 0, supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo "
+ "DeviceProductInfo{name=, manufacturerPnpId=QCM, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, "
+ "relativeAddress=null}, removeMode 0}\n"
+ " mRequestedMinimalPostProcessing=false\n";
DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0);
Assert.assertNotNull(displayInfo);
Assert.assertEquals(0, displayInfo.getDisplayId());
Assert.assertEquals(0, displayInfo.getRotation());
Assert.assertEquals(0, displayInfo.getLayerStack());
// FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported
Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags());
Assert.assertEquals(1440, displayInfo.getSize().getWidth());
Assert.assertEquals(3120, displayInfo.getSize().getHeight());
}
@Test
public void testParseDisplayInfoFromDumpsysDisplayWithRotation() {
/* @formatter:off */
String partialOutput = "Logical Displays: size=1\n"
+ " Display 0:\n"
+ "mDisplayId=0\n"
+ " mLayerStack=0\n"
+ " mHasContent=true\n"
+ " mDesiredDisplayModeSpecs={baseModeId=2 primaryRefreshRateRange=[90 90] appRequestRefreshRateRange=[90 90]}\n"
+ " mRequestedColorMode=0\n"
+ " mDisplayOffset=(0, 0)\n"
+ " mDisplayScalingDisabled=false\n"
+ " mPrimaryDisplayDevice=Built-in Screen\n"
+ " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, "
+ "real 1440 x 3120, largest app 1440 x 3120, smallest app 1440 x 3120, appVsyncOff 2000000, presDeadline 11111111, mode 2, "
+ "defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, width=1080, "
+ "height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], "
+ "mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, minimalPostProcessingSupported false, rotation 0, state ON, "
+ "type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 (515.154 x 514.597) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo DeviceProductInfo{name=, manufacturerPnpId=QCM, "
+ "productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, removeMode 0}\n"
+ " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, "
+ "FLAG_TRUSTED, real 3120 x 1440, largest app 3120 x 2983, smallest app 1440 x 1303, appVsyncOff 2000000, presDeadline 11111111, "
+ "mode 2, defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, "
+ "width=1080, height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities "
+ "HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, "
+ "minimalPostProcessingSupported false, rotation 3, state ON, type INTERNAL, uniqueId \"local:0\", app 3120 x 1440, density 600 "
+ "(515.154 x 514.597) dpi, layerStack 0, colorMode 0, supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo "
+ "DeviceProductInfo{name=, manufacturerPnpId=QCM, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, "
+ "relativeAddress=null}, removeMode 0}\n"
+ " mRequestedMinimalPostProcessing=false";
DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0);
Assert.assertNotNull(displayInfo);
Assert.assertEquals(0, displayInfo.getDisplayId());
Assert.assertEquals(3, displayInfo.getRotation());
Assert.assertEquals(0, displayInfo.getLayerStack());
// FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported
Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags());
Assert.assertEquals(3120, displayInfo.getSize().getWidth());
Assert.assertEquals(1440, displayInfo.getSize().getHeight());
}
@Test
public void testParseDisplayInfoFromDumpsysDisplayAPI31() {
/* @formatter:off */
String partialOutput = "Logical Displays: size=1\n"
+ " Display 0:\n"
+ " mDisplayId=0\n"
+ " mPhase=1\n"
+ " mLayerStack=0\n"
+ " mHasContent=true\n"
+ " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 "
+ "Infinity]}\n"
+ " mRequestedColorMode=0\n"
+ " mDisplayOffset=(0, 0)\n"
+ " mDisplayScalingDisabled=false\n"
+ " mPrimaryDisplayDevice=Built-in Screen\n"
+ " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, "
+ "FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff "
+ "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, "
+ "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, "
+ "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state "
+ "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, "
+ "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, "
+ "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n"
+ " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, "
+ "FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff "
+ "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, "
+ "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, "
+ "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state "
+ "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2148, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, "
+ "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, "
+ "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n"
+ " mRequestedMinimalPostProcessing=false\n"
+ " mFrameRateOverrides=[]\n"
+ " mPendingFrameRateOverrideUids={}\n";
DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0);
Assert.assertNotNull(displayInfo);
Assert.assertEquals(0, displayInfo.getDisplayId());
Assert.assertEquals(0, displayInfo.getRotation());
Assert.assertEquals(0, displayInfo.getLayerStack());
// FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported
Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags());
Assert.assertEquals(1080, displayInfo.getSize().getWidth());
Assert.assertEquals(2280, displayInfo.getSize().getHeight());
}
@Test
public void testParseDisplayInfoFromDumpsysDisplayAPI31NoFlags() {
/* @formatter:off */
String partialOutput = "Logical Displays: size=1\n"
+ " Display 0:\n"
+ " mDisplayId=0\n"
+ " mPhase=1\n"
+ " mLayerStack=0\n"
+ " mHasContent=true\n"
+ " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 "
+ "Infinity]}\n"
+ " mRequestedColorMode=0\n"
+ " mDisplayOffset=(0, 0)\n"
+ " mDisplayScalingDisabled=false\n"
+ " mPrimaryDisplayDevice=Built-in Screen\n"
+ " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, "
+ "real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff "
+ "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, "
+ "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, "
+ "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state "
+ "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, "
+ "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, "
+ "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n"
+ " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, "
+ "real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff "
+ "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, "
+ "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, "
+ "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state "
+ "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2148, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, "
+ "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, "
+ "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n"
+ " mRequestedMinimalPostProcessing=false\n"
+ " mFrameRateOverrides=[]\n"
+ " mPendingFrameRateOverrideUids={}\n";
DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0);
Assert.assertNotNull(displayInfo);
Assert.assertEquals(0, displayInfo.getDisplayId());
Assert.assertEquals(0, displayInfo.getRotation());
Assert.assertEquals(0, displayInfo.getLayerStack());
Assert.assertEquals(0, displayInfo.getFlags());
Assert.assertEquals(1080, displayInfo.getSize().getWidth());
Assert.assertEquals(2280, displayInfo.getSize().getHeight());
}
@Test
public void testParseDisplayInfoFromDumpsysDisplayAPI29WithNoFlags() {
/* @formatter:off */
String partialOutput = "Logical Displays: size=2\n"
+ " Display 0:\n"
+ " mDisplayId=0\n"
+ " mLayerStack=0\n"
+ " mHasContent=true\n"
+ " mAllowedDisplayModes=[1]\n"
+ " mRequestedColorMode=0\n"
+ " mDisplayOffset=(0, 0)\n"
+ " mDisplayScalingDisabled=false\n"
+ " mPrimaryDisplayDevice=Built-in Screen\n"
+ " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, "
+ "real 3664 x 1920, largest app 3664 x 1920, smallest app 3664 x 1920, mode 61, defaultMode 61, modes ["
+ "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, "
+ "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], "
+ "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, "
+ "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, "
+ "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n"
+ " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, "
+ "real 3664 x 1920, largest app 3664 x 3620, smallest app 1920 x 1876, mode 61, defaultMode 61, modes ["
+ "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, "
+ "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], "
+ "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, "
+ "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, "
+ "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n"
+ " Display 31:\n"
+ " mDisplayId=31\n"
+ " mLayerStack=31\n"
+ " mHasContent=true\n"
+ " mAllowedDisplayModes=[92]\n"
+ " mRequestedColorMode=0\n"
+ " mDisplayOffset=(0, 0)\n"
+ " mDisplayScalingDisabled=false\n"
+ " mPrimaryDisplayDevice=PanelLayer-#main\n"
+ " mBaseDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId "
+ "\"virtual:com.test.system,10040,PanelLayer-#main,0\", app 800 x 110, real 800 x 110, largest app 800 x 110, smallest app 800 x "
+ "110, mode 92, defaultMode 92, modes [{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], "
+ "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, "
+ "type VIRTUAL, state ON, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n"
+ " mOverrideDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId "
+ "\"virtual:com.test.system,10040,PanelLayer-#main,0\", app 800 x 110, real 800 x 110, largest app 800 x 800, smallest app 110 x "
+ "110, mode 92, defaultMode 92, modes [{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], "
+ "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, "
+ "type VIRTUAL, state OFF, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n";
DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 31);
Assert.assertNotNull(displayInfo);
Assert.assertEquals(31, displayInfo.getDisplayId());
Assert.assertEquals(0, displayInfo.getRotation());
Assert.assertEquals(31, displayInfo.getLayerStack());
Assert.assertEquals(0, displayInfo.getFlags());
Assert.assertEquals(800, displayInfo.getSize().getWidth());
Assert.assertEquals(110, displayInfo.getSize().getHeight());
}
}

View File

@ -126,8 +126,8 @@ public class ControlMessageReaderTest {
dos.writeInt(1026); dos.writeInt(1026);
dos.writeShort(1080); dos.writeShort(1080);
dos.writeShort(1920); dos.writeShort(1920);
dos.writeInt(1); dos.writeShort(0); // 0.0f encoded as i16
dos.writeInt(-1); dos.writeShort(0x8000); // -1.0f encoded as i16
dos.writeInt(1); dos.writeInt(1);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -143,8 +143,8 @@ public class ControlMessageReaderTest {
Assert.assertEquals(1026, event.getPosition().getPoint().getY()); Assert.assertEquals(1026, event.getPosition().getPoint().getY());
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(1, event.getHScroll()); Assert.assertEquals(0f, event.getHScroll(), 0f);
Assert.assertEquals(-1, event.getVScroll()); Assert.assertEquals(-1f, event.getVScroll(), 0f);
Assert.assertEquals(1, event.getButtons()); Assert.assertEquals(1, event.getButtons());
} }