diff --git a/DEVELOP.md b/DEVELOP.md index 38c9c63e..dea8137d 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -32,7 +32,7 @@ The server is a Java application (with a [`public static void main(String... args)`][main] method), compiled against the Android framework, and executed as `shell` on the Android device. -[main]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/Server.java#L100 +[main]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123 To run such a Java application, the classes must be [_dexed_][dex] (typically, to `classes.dex`). If `my.package.MainClass` is the main class, compiled to @@ -65,17 +65,20 @@ They can be called using reflection though. The communication with hidden components is provided by [_wrappers_ classes][wrappers] and [aidl]. [hidden]: https://stackoverflow.com/a/31908373/1987178 -[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers -[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/aidl/android/view +[wrappers]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers +[aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view ### Threading -The server uses 2 threads: +The server uses 3 threads: - the **main** thread, encoding and streaming the video to the client; - - the **controller** thread, listening for _control events_ (typically, - keyboard and mouse events) from the client. + - the **controller** thread, listening for _control messages_ (typically, + keyboard and mouse events) from the client; + - the **receiver** thread (managed by the controller), sending _device messges_ + to the clients (currently, it is only used to send the device clipboard + content). Since the video encoding is typically hardware, there would be no benefit in encoding and streaming in two different threads. @@ -89,9 +92,9 @@ The video is encoded using the [`MediaCodec`] API. The codec takes its input from a [surface] associated to the display, and writes the resulting H.264 stream to the provided output stream (the socket connected to the client). -[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java [`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html -[surface]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L69-L70 +[surface]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69 On device [rotation], the codec, surface and display are reinitialized, and a new video stream is produced. @@ -105,31 +108,30 @@ because it avoids to send unnecessary frames, but there are drawbacks: Both problems are [solved][repeat] by the flag [`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. -[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 -[repeat]: -https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148 +[rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 +[repeat]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148 [repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER ### Input events injection -_Control events_ are received from the client by the [`EventController`] (run in -a separate thread). There are 5 types of input events: +_Control messages_ are received from the client by the [`Controller`] (run in a +separate thread). There are several types of input events: - keycode (cf [`KeyEvent`]), - text (special characters may not be handled by keycodes directly), - mouse motion/click, - mouse scroll, - - custom command (e.g. to switch the screen on). + - other commands (e.g. to switch the screen on or to copy the clipboard). -All of them may need to inject input events to the system. To do so, they use -the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our +Some of them need to inject input events to the system. To do so, they use the +_hidden_ method [`InputManager.injectInputEvent`] (exposed by our [`InputManager` wrapper][inject-wrapper]). -[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/EventController.java#L66 +[`Controller`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Controller.java#L81 [`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html [`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html [`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857 -[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 +[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 @@ -146,8 +148,8 @@ The video stream is decoded by [libav] (FFmpeg). ### Initialization On startup, in addition to _libav_ and _SDL_ initialization, the client must -push and start the server on the device, and open a socket so that they may -communicate. +push and start the server on the device, and open two sockets (one for the video +stream, one for control) so that they may communicate. Note that the client-server roles are expressed at the application level: @@ -180,15 +182,18 @@ the connection from the server (see commit [90a46b4]). ### Threading -The client uses 3 threads: +The client uses 4 threads: - the **main** thread, executing the SDL event loop, - the **stream** thread, receiving the video and used for decoding and recording, - - the **controller** thread, sending _control events_ to the server. + - the **controller** thread, sending _control messages_ to the server, + - the **receiver** thread (managed by the controller), receiving _device + messages_ from the client. In addition, another thread can be started if necessary to handle APK -installation or file push requests (via drag&drop on the main window). +installation or file push requests (via drag&drop on the main window) or to +print the framerate regularly in the console. @@ -212,10 +217,10 @@ to decode a new frame while the main thread renders the last one. If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw H.264 packet to the output video file. -[stream]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/stream.h -[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/decoder.h -[video_buffer]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/video_buffer.h -[recorder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/recorder.h +[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h +[decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h +[video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h +[recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h ``` +----------+ +----------+ @@ -229,20 +234,19 @@ H.264 packet to the output video file. ### Controller -The [controller] is responsible to send _control events_ to the device. It runs -in a separate thread, to avoid I/O on the main thread. +The [controller] is responsible to send _control messages_ to the device. It +runs in a separate thread, to avoid I/O on the main thread. On SDL event, received on the main thread, the [input manager][inputmanager] -creates appropriate [_control events_][controlevent]. It is responsible to +creates appropriate [_control messages_][controlmsg]. It is responsible to convert SDL events to Android events (using [convert]). It pushes the _control -events_ to a blocking queue hold by the controller. On its own thread, the -controller takes events from the queue, that it serializes and sends to the -client. +messages_ to a queue hold by the controller. On its own thread, the controller +takes messages from the queue, that it serializes and sends to the client. -[controller]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/controller.h -[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/control_event.h -[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/input_manager.h -[convert]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/convert.h +[controller]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/controller.h +[controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h +[inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h +[convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h ### UI and event loop @@ -253,10 +257,9 @@ thread. Events are handled in the [event loop], which either updates the [screen] or delegates to the [input manager][inputmanager]. -[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c -[event loop]: -https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c#L187 -[screen]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/screen.h +[scrcpy]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c +[event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201 +[screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h ## Hack diff --git a/FAQ.md b/FAQ.md index 76f572ce..4b04d228 100644 --- a/FAQ.md +++ b/FAQ.md @@ -19,10 +19,6 @@ Windows may need some [drivers] to detect your device. [drivers]: https://developer.android.com/studio/run/oem-usb.html -If you still encounter problems, please see [issue 9]. - -[issue 9]: https://github.com/Genymobile/scrcpy/issues/9 - ### Mouse clicks do not work diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 0f7b14a6..7955c544 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -57,7 +57,7 @@ build-win32: prepare-deps-win32 --buildtype release --strip -Db_lto=true \ -Dcrossbuild_windows=true \ -Dbuild_server=false \ - -Doverride_server_path=scrcpy-server.jar ) + -Dportable=true ) ninja -C "$(WIN32_BUILD_DIR)" build-win32-noconsole: prepare-deps-win32 @@ -68,7 +68,7 @@ build-win32-noconsole: prepare-deps-win32 -Dcrossbuild_windows=true \ -Dbuild_server=false \ -Dwindows_noconsole=true \ - -Doverride_server_path=scrcpy-server.jar ) + -Dportable=true ) ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)" prepare-deps-win64: @@ -81,7 +81,7 @@ build-win64: prepare-deps-win64 --buildtype release --strip -Db_lto=true \ -Dcrossbuild_windows=true \ -Dbuild_server=false \ - -Doverride_server_path=scrcpy-server.jar ) + -Dportable=true ) ninja -C "$(WIN64_BUILD_DIR)" build-win64-noconsole: prepare-deps-win64 @@ -92,7 +92,7 @@ build-win64-noconsole: prepare-deps-win64 -Dcrossbuild_windows=true \ -Dbuild_server=false \ -Dwindows_noconsole=true \ - -Doverride_server_path=scrcpy-server.jar ) + -Dportable=true ) ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)" dist-win32: build-server build-win32 build-win32-noconsole @@ -100,28 +100,29 @@ dist-win32: build-server build-win32 build-win32-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/SDL2-2.0.9/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.8/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 build-win64-noconsole mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/SDL2-2.0.9/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.8/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)"; \ diff --git a/README.md b/README.md index 5daafb2e..5026ebe2 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,33 @@ scrcpy --no-control scrcpy -n ``` +### Turn screen off + +It is possible to turn the device screen off while mirroring on start with a +command-line option: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Or by pressing `Ctrl`+`o` at any time. + +To turn it back on, press `POWER` (or `Ctrl`+`p`). + + +### Render expired frames + +By default, to minimize latency, _scrcpy_ always renders the last decoded frame +available, and drops any previous one. + +To force the rendering of all frames (at a cost of a possible increased +latency), use: + +```bash +scrcpy --render-expired-frames +``` + ### Forward audio @@ -284,10 +311,13 @@ you are interested, see [issue 14]. | click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) | | click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) | | click on `POWER` | `Ctrl`+`p` | - | turn screen on | _Right-click²_ | + | power on | _Right-click²_ | + | turn device screen off (keep mirroring)| `Ctrl`+`o` | | expand notification panel | `Ctrl`+`n` | | collapse notification panel | `Ctrl`+`Shift`+`n` | + | copy device clipboard to computer | `Ctrl`+`c` | | paste computer clipboard to device | `Ctrl`+`v` | + | copy computer clipboard to device | `Ctrl`+`Shift+`v` | | enable/disable FPS counter (on stdout) | `Ctrl`+`i` | _¹Double-click on black borders to remove them._ @@ -301,8 +331,8 @@ To use a specific _adb_ binary, configure its path in the environment variable ADB=/path/to/adb scrcpy -To override the path of the `scrcpy-server.jar` file (it can be [useful] on -Windows), configure its path in `SCRCPY_SERVER_PATH`. +To override the path of the `scrcpy-server.jar` file, configure its path in +`SCRCPY_SERVER_PATH`. [useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 diff --git a/app/meson.build b/app/meson.build index 5942fd08..02d24a34 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,16 +1,17 @@ src = [ 'src/main.c', 'src/command.c', - 'src/control_event.c', + 'src/control_msg.c', 'src/controller.c', 'src/convert.c', 'src/decoder.c', 'src/device.c', + 'src/device_msg.c', 'src/file_handler.c', 'src/fps_counter.c', 'src/input_manager.c', - 'src/lock_util.c', 'src/net.c', + 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', @@ -92,21 +93,9 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version()) # the prefix used during configuration (meson --prefix=PREFIX) conf.set_quoted('PREFIX', get_option('prefix')) -# the path of the server, which will be appended to the prefix -# ignored if OVERRIDE_SERVER_PATH if defined -# must be consistent with the install_dir in server/meson.build -conf.set_quoted('PREFIXED_SERVER_PATH', '/share/scrcpy/scrcpy-server.jar') - -# the path of the server to be used "as is" -# this is useful for building a "portable" version (with the server in the same -# directory as the client) -override_server_path = get_option('override_server_path') -if override_server_path != '' - conf.set_quoted('OVERRIDE_SERVER_PATH', override_server_path) -else - # undefine it - conf.set('OVERRIDE_SERVER_PATH', false) -endif +# build a "portable" version (with scrcpy-server.jar accessible from the same +# directory as the executable) +conf.set('PORTABLE', get_option('portable')) # the default client TCP port for the "adb reverse" tunnel # overridden by option --port @@ -120,11 +109,6 @@ conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited # overridden by option --bit-rate conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps -# whether the app should always display the most recent available frame, even -# if the previous one has not been displayed -# SKIP_FRAMES improves latency at the cost of framerate -conf.set('SKIP_FRAMES', get_option('skip_frames')) - # enable High DPI support conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) @@ -143,18 +127,38 @@ else link_args = [] endif -executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, c_args: c_args, link_args: link_args) +executable('scrcpy', src, + dependencies: dependencies, + include_directories: src_dir, + install: true, + c_args: c_args, + link_args: link_args) ### TESTS tests = [ - ['test_control_event_queue', ['tests/test_control_event_queue.c', 'src/control_event.c']], - ['test_control_event_serialize', ['tests/test_control_event_serialize.c', 'src/control_event.c']], - ['test_strutil', ['tests/test_strutil.c', 'src/str_util.c']], + ['test_cbuf', [ + 'tests/test_cbuf.c', + ]], + ['test_control_event_serialize', [ + 'tests/test_control_msg_serialize.c', + 'src/control_msg.c', + 'src/str_util.c' + ]], + ['test_device_event_deserialize', [ + 'tests/test_device_msg_deserialize.c', + 'src/device_msg.c' + ]], + ['test_strutil', [ + 'tests/test_strutil.c', + 'src/str_util.c' + ]], ] foreach t : tests - exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies) + exe = executable(t[0], t[1], + include_directories: src_dir, + dependencies: dependencies) test(t[0], exe) endforeach diff --git a/app/src/buffer_util.h b/app/src/buffer_util.h index 5d94deef..a79014b1 100644 --- a/app/src/buffer_util.h +++ b/app/src/buffer_util.h @@ -18,13 +18,18 @@ buffer_write32be(uint8_t *buf, uint32_t value) { buf[3] = value; } +static inline uint16_t +buffer_read16be(const uint8_t *buf) { + return (buf[0] << 8) | buf[1]; +} + static inline uint32_t -buffer_read32be(uint8_t *buf) { +buffer_read32be(const uint8_t *buf) { return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; } static inline -uint64_t buffer_read64be(uint8_t *buf) { +uint64_t buffer_read64be(const uint8_t *buf) { uint32_t msb = buffer_read32be(buf); uint32_t lsb = buffer_read32be(&buf[4]); return ((uint64_t) msb << 32) | lsb; diff --git a/app/src/cbuf.h b/app/src/cbuf.h new file mode 100644 index 00000000..5d9fe4ae --- /dev/null +++ b/app/src/cbuf.h @@ -0,0 +1,50 @@ +// generic circular buffer (bounded queue) implementation +#ifndef CBUF_H +#define CBUF_H + +#include +#include + +// To define a circular buffer type of 20 ints: +// typedef CBUF(int, 20) my_cbuf_t; +// +// data has length CAP + 1 to distinguish empty vs full. +#define CBUF(TYPE, CAP) { \ + TYPE data[(CAP) + 1]; \ + size_t head; \ + size_t tail; \ +} + +#define cbuf_size_(PCBUF) \ + (sizeof((PCBUF)->data) / sizeof(*(PCBUF)->data)) + +#define cbuf_is_empty(PCBUF) \ + ((PCBUF)->head == (PCBUF)->tail) + +#define cbuf_is_full(PCBUF) \ + (((PCBUF)->head + 1) % cbuf_size_(PCBUF) == (PCBUF)->tail) + +#define cbuf_init(PCBUF) \ + (void) ((PCBUF)->head = (PCBUF)->tail = 0) + +#define cbuf_push(PCBUF, ITEM) \ + ({ \ + bool ok = !cbuf_is_full(PCBUF); \ + if (ok) { \ + (PCBUF)->data[(PCBUF)->head] = (ITEM); \ + (PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \ + } \ + ok; \ + }) \ + +#define cbuf_take(PCBUF, PITEM) \ + ({ \ + bool ok = !cbuf_is_empty(PCBUF); \ + if (ok) { \ + *(PITEM) = (PCBUF)->data[(PCBUF)->tail]; \ + (PCBUF)->tail = ((PCBUF)->tail + 1) % cbuf_size_(PCBUF); \ + } \ + ok; \ + }) + +#endif diff --git a/app/src/command.c b/app/src/command.c index 2c6d45aa..4cb2e408 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -69,7 +69,7 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) { "path in the ADB environment variable)"); break; case PROCESS_SUCCESS: - /* do nothing */ + // do nothing break; } } @@ -147,7 +147,7 @@ adb_push(const char *serial, const char *local, const char *remote) { } remote = strquote(remote); if (!remote) { - free((void *) local); + SDL_free((void *) local); return PROCESS_NONE; } #endif @@ -156,8 +156,8 @@ adb_push(const char *serial, const char *local, const char *remote) { process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); #ifdef __WINDOWS__ - free((void *) remote); - free((void *) local); + SDL_free((void *) remote); + SDL_free((void *) local); #endif return proc; @@ -178,7 +178,7 @@ adb_install(const char *serial, const char *local) { process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); #ifdef __WINDOWS__ - free((void *) local); + SDL_free((void *) local); #endif return proc; diff --git a/app/src/command.h b/app/src/command.h index 90eb7cb2..db6358da 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -9,6 +9,7 @@ // not needed here, but winsock2.h must never be included AFTER windows.h # include # include +# define PATH_SEPARATOR '\\' # define PRIexitcode "lu" // # ifdef _WIN64 @@ -23,6 +24,7 @@ #else # include +# define PATH_SEPARATOR '/' # define PRIsizet "zu" # define PRIexitcode "d" # define PROCESS_NONE -1 @@ -74,6 +76,11 @@ adb_install(const char *serial, const char *local); // convenience function to wait for a successful process execution // automatically log process errors with the provided process name bool -process_check_success(process_t process, const char *name); +process_check_success(process_t proc, const char *name); + +// return the absolute path of the executable (the scrcpy binary) +// may be NULL on error; to be freed by SDL_free +char * +get_executable_path(void); #endif diff --git a/app/src/control_event.c b/app/src/control_event.c deleted file mode 100644 index 40de6efc..00000000 --- a/app/src/control_event.c +++ /dev/null @@ -1,110 +0,0 @@ -#include "control_event.h" - -#include - -#include "buffer_util.h" -#include "lock_util.h" -#include "log.h" - -static void -write_position(uint8_t *buf, const struct position *position) { - buffer_write32be(&buf[0], position->point.x); - buffer_write32be(&buf[4], position->point.y); - buffer_write16be(&buf[8], position->screen_size.width); - buffer_write16be(&buf[10], position->screen_size.height); -} - -int -control_event_serialize(const struct control_event *event, unsigned char *buf) { - buf[0] = event->type; - switch (event->type) { - case CONTROL_EVENT_TYPE_KEYCODE: - buf[1] = event->keycode_event.action; - buffer_write32be(&buf[2], event->keycode_event.keycode); - buffer_write32be(&buf[6], event->keycode_event.metastate); - return 10; - case CONTROL_EVENT_TYPE_TEXT: { - // write length (2 bytes) + string (non nul-terminated) - size_t len = strlen(event->text_event.text); - if (len > TEXT_MAX_LENGTH) { - // injecting a text takes time, so limit the text length - len = TEXT_MAX_LENGTH; - } - buffer_write16be(&buf[1], (uint16_t) len); - memcpy(&buf[3], event->text_event.text, len); - return 3 + len; - } - case CONTROL_EVENT_TYPE_MOUSE: - buf[1] = event->mouse_event.action; - buffer_write32be(&buf[2], event->mouse_event.buttons); - write_position(&buf[6], &event->mouse_event.position); - return 18; - case CONTROL_EVENT_TYPE_SCROLL: - write_position(&buf[1], &event->scroll_event.position); - buffer_write32be(&buf[13], (uint32_t) event->scroll_event.hscroll); - buffer_write32be(&buf[17], (uint32_t) event->scroll_event.vscroll); - return 21; - case CONTROL_EVENT_TYPE_COMMAND: - buf[1] = event->command_event.action; - return 2; - default: - LOGW("Unknown event type: %u", (unsigned) event->type); - return 0; - } -} - -void -control_event_destroy(struct control_event *event) { - if (event->type == CONTROL_EVENT_TYPE_TEXT) { - SDL_free(event->text_event.text); - } -} - -bool -control_event_queue_is_empty(const struct control_event_queue *queue) { - return queue->head == queue->tail; -} - -bool -control_event_queue_is_full(const struct control_event_queue *queue) { - return (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE == queue->tail; -} - -bool -control_event_queue_init(struct control_event_queue *queue) { - queue->head = 0; - queue->tail = 0; - // the current implementation may not fail - return true; -} - -void -control_event_queue_destroy(struct control_event_queue *queue) { - int i = queue->tail; - while (i != queue->head) { - control_event_destroy(&queue->data[i]); - i = (i + 1) % CONTROL_EVENT_QUEUE_SIZE; - } -} - -bool -control_event_queue_push(struct control_event_queue *queue, - const struct control_event *event) { - if (control_event_queue_is_full(queue)) { - return false; - } - queue->data[queue->head] = *event; - queue->head = (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE; - return true; -} - -bool -control_event_queue_take(struct control_event_queue *queue, - struct control_event *event) { - if (control_event_queue_is_empty(queue)) { - return false; - } - *event = queue->data[queue->tail]; - queue->tail = (queue->tail + 1) % CONTROL_EVENT_QUEUE_SIZE; - return true; -} diff --git a/app/src/control_event.h b/app/src/control_event.h deleted file mode 100644 index 2a33244b..00000000 --- a/app/src/control_event.h +++ /dev/null @@ -1,91 +0,0 @@ -#ifndef CONTROLEVENT_H -#define CONTROLEVENT_H - -#include -#include -#include - -#include "android/input.h" -#include "android/keycodes.h" -#include "common.h" - -#define CONTROL_EVENT_QUEUE_SIZE 64 -#define TEXT_MAX_LENGTH 300 -#define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH - -enum control_event_type { - CONTROL_EVENT_TYPE_KEYCODE, - CONTROL_EVENT_TYPE_TEXT, - CONTROL_EVENT_TYPE_MOUSE, - CONTROL_EVENT_TYPE_SCROLL, - CONTROL_EVENT_TYPE_COMMAND, -}; - -enum control_event_command { - CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON, - CONTROL_EVENT_COMMAND_EXPAND_NOTIFICATION_PANEL, - CONTROL_EVENT_COMMAND_COLLAPSE_NOTIFICATION_PANEL, -}; - -struct control_event { - enum control_event_type type; - union { - struct { - enum android_keyevent_action action; - enum android_keycode keycode; - enum android_metastate metastate; - } keycode_event; - struct { - char *text; // owned, to be freed by SDL_free() - } text_event; - struct { - enum android_motionevent_action action; - enum android_motionevent_buttons buttons; - struct position position; - } mouse_event; - struct { - struct position position; - int32_t hscroll; - int32_t vscroll; - } scroll_event; - struct { - enum control_event_command action; - } command_event; - }; -}; - -struct control_event_queue { - struct control_event data[CONTROL_EVENT_QUEUE_SIZE]; - int head; - int tail; -}; - -// buf size must be at least SERIALIZED_EVENT_MAX_SIZE -int -control_event_serialize(const struct control_event *event, unsigned char *buf); - -bool -control_event_queue_init(struct control_event_queue *queue); - -void -control_event_queue_destroy(struct control_event_queue *queue); - -bool -control_event_queue_is_empty(const struct control_event_queue *queue); - -bool -control_event_queue_is_full(const struct control_event_queue *queue); - -// event is copied, the queue does not use the event after the function returns -bool -control_event_queue_push(struct control_event_queue *queue, - const struct control_event *event); - -bool -control_event_queue_take(struct control_event_queue *queue, - struct control_event *event); - -void -control_event_destroy(struct control_event *event); - -#endif diff --git a/app/src/control_msg.c b/app/src/control_msg.c new file mode 100644 index 00000000..9c3d9849 --- /dev/null +++ b/app/src/control_msg.c @@ -0,0 +1,86 @@ +#include "control_msg.h" + +#include + +#include "buffer_util.h" +#include "log.h" +#include "str_util.h" + +static void +write_position(uint8_t *buf, const struct position *position) { + buffer_write32be(&buf[0], position->point.x); + buffer_write32be(&buf[4], position->point.y); + buffer_write16be(&buf[8], position->screen_size.width); + buffer_write16be(&buf[10], position->screen_size.height); +} + +// write length (2 bytes) + string (non nul-terminated) +static size_t +write_string(const char *utf8, size_t max_len, unsigned char *buf) { + size_t len = utf8_truncation_index(utf8, max_len); + buffer_write16be(buf, (uint16_t) len); + memcpy(&buf[2], utf8, len); + return 2 + len; +} + +size_t +control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { + buf[0] = msg->type; + switch (msg->type) { + case CONTROL_MSG_TYPE_INJECT_KEYCODE: + buf[1] = msg->inject_keycode.action; + buffer_write32be(&buf[2], msg->inject_keycode.keycode); + buffer_write32be(&buf[6], msg->inject_keycode.metastate); + return 10; + case CONTROL_MSG_TYPE_INJECT_TEXT: { + size_t len = write_string(msg->inject_text.text, + CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]); + return 1 + len; + } + case CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT: + buf[1] = msg->inject_mouse_event.action; + buffer_write32be(&buf[2], msg->inject_mouse_event.buttons); + write_position(&buf[6], &msg->inject_mouse_event.position); + return 18; + case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: + write_position(&buf[1], &msg->inject_scroll_event.position); + buffer_write32be(&buf[13], + (uint32_t) msg->inject_scroll_event.hscroll); + buffer_write32be(&buf[17], + (uint32_t) msg->inject_scroll_event.vscroll); + return 21; + case CONTROL_MSG_TYPE_SET_CLIPBOARD: { + size_t len = write_string(msg->inject_text.text, + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, + &buf[1]); + return 1 + len; + } + case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + buf[1] = msg->set_screen_power_mode.mode; + return 2; + case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: + case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: + case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: + case CONTROL_MSG_TYPE_GET_CLIPBOARD: + // no additional data + return 1; + default: + LOGW("Unknown message type: %u", (unsigned) msg->type); + return 0; + } +} + +void +control_msg_destroy(struct control_msg *msg) { + switch (msg->type) { + case CONTROL_MSG_TYPE_INJECT_TEXT: + SDL_free(msg->inject_text.text); + break; + case CONTROL_MSG_TYPE_SET_CLIPBOARD: + SDL_free(msg->set_clipboard.text); + break; + default: + // do nothing + break; + } +} diff --git a/app/src/control_msg.h b/app/src/control_msg.h new file mode 100644 index 00000000..e7fdfc4c --- /dev/null +++ b/app/src/control_msg.h @@ -0,0 +1,74 @@ +#ifndef CONTROLMSG_H +#define CONTROLMSG_H + +#include +#include +#include + +#include "android/input.h" +#include "android/keycodes.h" +#include "common.h" + +#define CONTROL_MSG_TEXT_MAX_LENGTH 300 +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 +#define CONTROL_MSG_SERIALIZED_MAX_SIZE \ + (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) + +enum control_msg_type { + CONTROL_MSG_TYPE_INJECT_KEYCODE, + CONTROL_MSG_TYPE_INJECT_TEXT, + CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_GET_CLIPBOARD, + CONTROL_MSG_TYPE_SET_CLIPBOARD, + CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, +}; + +enum screen_power_mode { + // see + SCREEN_POWER_MODE_OFF = 0, + SCREEN_POWER_MODE_NORMAL = 2, +}; + +struct control_msg { + enum control_msg_type type; + union { + struct { + enum android_keyevent_action action; + enum android_keycode keycode; + enum android_metastate metastate; + } inject_keycode; + struct { + char *text; // owned, to be freed by SDL_free() + } inject_text; + struct { + enum android_motionevent_action action; + enum android_motionevent_buttons buttons; + struct position position; + } inject_mouse_event; + struct { + struct position position; + int32_t hscroll; + int32_t vscroll; + } inject_scroll_event; + struct { + char *text; // owned, to be freed by SDL_free() + } set_clipboard; + struct { + enum screen_power_mode mode; + } set_screen_power_mode; + }; +}; + +// buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE +// return the number of bytes written +size_t +control_msg_serialize(const struct control_msg *msg, unsigned char *buf); + +void +control_msg_destroy(struct control_msg *msg); + +#endif diff --git a/app/src/controller.c b/app/src/controller.c index 8b95e88c..4b1f4c8b 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -7,21 +7,25 @@ #include "log.h" bool -controller_init(struct controller *controller, socket_t video_socket) { - if (!control_event_queue_init(&controller->queue)) { +controller_init(struct controller *controller, socket_t control_socket) { + cbuf_init(&controller->queue); + + if (!receiver_init(&controller->receiver, control_socket)) { return false; } if (!(controller->mutex = SDL_CreateMutex())) { + receiver_destroy(&controller->receiver); return false; } - if (!(controller->event_cond = SDL_CreateCond())) { + if (!(controller->msg_cond = SDL_CreateCond())) { + receiver_destroy(&controller->receiver); SDL_DestroyMutex(controller->mutex); return false; } - controller->video_socket = video_socket; + controller->control_socket = control_socket; controller->stopped = false; return true; @@ -29,34 +33,39 @@ controller_init(struct controller *controller, socket_t video_socket) { void controller_destroy(struct controller *controller) { - SDL_DestroyCond(controller->event_cond); + SDL_DestroyCond(controller->msg_cond); SDL_DestroyMutex(controller->mutex); - control_event_queue_destroy(&controller->queue); + + struct control_msg msg; + while (cbuf_take(&controller->queue, &msg)) { + control_msg_destroy(&msg); + } + + receiver_destroy(&controller->receiver); } bool -controller_push_event(struct controller *controller, - const struct control_event *event) { - bool res; +controller_push_msg(struct controller *controller, + const struct control_msg *msg) { mutex_lock(controller->mutex); - bool was_empty = control_event_queue_is_empty(&controller->queue); - res = control_event_queue_push(&controller->queue, event); + bool was_empty = cbuf_is_empty(&controller->queue); + bool res = cbuf_push(&controller->queue, *msg); if (was_empty) { - cond_signal(controller->event_cond); + cond_signal(controller->msg_cond); } mutex_unlock(controller->mutex); return res; } static bool -process_event(struct controller *controller, - const struct control_event *event) { - unsigned char serialized_event[SERIALIZED_EVENT_MAX_SIZE]; - int length = control_event_serialize(event, serialized_event); +process_msg(struct controller *controller, + const struct control_msg *msg) { + unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int length = control_msg_serialize(msg, serialized_msg); if (!length) { return false; } - int w = net_send_all(controller->video_socket, serialized_event, length); + int w = net_send_all(controller->control_socket, serialized_msg, length); return w == length; } @@ -66,25 +75,23 @@ run_controller(void *data) { for (;;) { mutex_lock(controller->mutex); - while (!controller->stopped - && control_event_queue_is_empty(&controller->queue)) { - cond_wait(controller->event_cond, controller->mutex); + while (!controller->stopped && cbuf_is_empty(&controller->queue)) { + cond_wait(controller->msg_cond, controller->mutex); } if (controller->stopped) { - // stop immediately, do not process further events + // stop immediately, do not process further msgs mutex_unlock(controller->mutex); break; } - struct control_event event; - bool non_empty = control_event_queue_take(&controller->queue, - &event); + struct control_msg msg; + bool non_empty = cbuf_take(&controller->queue, &msg); SDL_assert(non_empty); mutex_unlock(controller->mutex); - bool ok = process_event(controller, &event); - control_event_destroy(&event); + bool ok = process_msg(controller, &msg); + control_msg_destroy(&msg); if (!ok) { - LOGD("Cannot write event to socket"); + LOGD("Cannot write msg to socket"); break; } } @@ -102,6 +109,12 @@ controller_start(struct controller *controller) { return false; } + if (!receiver_start(&controller->receiver)) { + controller_stop(controller); + SDL_WaitThread(controller->thread, NULL); + return false; + } + return true; } @@ -109,11 +122,12 @@ void controller_stop(struct controller *controller) { mutex_lock(controller->mutex); controller->stopped = true; - cond_signal(controller->event_cond); + cond_signal(controller->msg_cond); mutex_unlock(controller->mutex); } void controller_join(struct controller *controller) { SDL_WaitThread(controller->thread, NULL); + receiver_join(&controller->receiver); } diff --git a/app/src/controller.h b/app/src/controller.h index 2f7696e3..ae13e39f 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -1,25 +1,29 @@ -#ifndef CONTROL_H -#define CONTROL_H - -#include "control_event.h" +#ifndef CONTROLLER_H +#define CONTROLLER_H #include #include #include +#include "cbuf.h" +#include "control_msg.h" #include "net.h" +#include "receiver.h" + +struct control_msg_queue CBUF(struct control_msg, 64); struct controller { - socket_t video_socket; + socket_t control_socket; SDL_Thread *thread; SDL_mutex *mutex; - SDL_cond *event_cond; + SDL_cond *msg_cond; bool stopped; - struct control_event_queue queue; + struct control_msg_queue queue; + struct receiver receiver; }; bool -controller_init(struct controller *controller, socket_t video_socket); +controller_init(struct controller *controller, socket_t control_socket); void controller_destroy(struct controller *controller); @@ -33,9 +37,8 @@ controller_stop(struct controller *controller); void controller_join(struct controller *controller); -// expose simple API to hide control_event_queue bool -controller_push_event(struct controller *controller, - const struct control_event *event); +controller_push_msg(struct controller *controller, + const struct control_msg *msg); #endif diff --git a/app/src/convert.c b/app/src/convert.c index 504befe0..adf6d400 100644 --- a/app/src/convert.c +++ b/app/src/convert.c @@ -159,19 +159,19 @@ convert_mouse_buttons(uint32_t state) { bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, - struct control_event *to) { - to->type = CONTROL_EVENT_TYPE_KEYCODE; + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; - if (!convert_keycode_action(from->type, &to->keycode_event.action)) { + if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { return false; } uint16_t mod = from->keysym.mod; - if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode, mod)) { + if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { return false; } - to->keycode_event.metastate = convert_meta_state(mod); + to->inject_keycode.metastate = convert_meta_state(mod); return true; } @@ -179,17 +179,18 @@ input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_event *to) { - to->type = CONTROL_EVENT_TYPE_MOUSE; + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; - if (!convert_mouse_action(from->type, &to->mouse_event.action)) { + if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) { return false; } - to->mouse_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); - to->mouse_event.position.screen_size = screen_size; - to->mouse_event.position.point.x = from->x; - to->mouse_event.position.point.y = from->y; + to->inject_mouse_event.buttons = + convert_mouse_buttons(SDL_BUTTON(from->button)); + to->inject_mouse_event.position.screen_size = screen_size; + to->inject_mouse_event.position.point.x = from->x; + to->inject_mouse_event.position.point.y = from->y; return true; } @@ -197,13 +198,13 @@ mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_event *to) { - to->type = CONTROL_EVENT_TYPE_MOUSE; - to->mouse_event.action = AMOTION_EVENT_ACTION_MOVE; - to->mouse_event.buttons = convert_mouse_buttons(from->state); - to->mouse_event.position.screen_size = screen_size; - to->mouse_event.position.point.x = from->x; - to->mouse_event.position.point.y = from->y; + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; + to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE; + to->inject_mouse_event.buttons = convert_mouse_buttons(from->state); + to->inject_mouse_event.position.screen_size = screen_size; + to->inject_mouse_event.position.point.x = from->x; + to->inject_mouse_event.position.point.y = from->y; return true; } @@ -211,17 +212,17 @@ mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, struct position position, - struct control_event *to) { - to->type = CONTROL_EVENT_TYPE_SCROLL; + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; - to->scroll_event.position = position; + to->inject_scroll_event.position = position; int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; // SDL behavior seems inconsistent between horizontal and vertical scrolling // so reverse the horizontal // - to->scroll_event.hscroll = -mul * from->x; - to->scroll_event.vscroll = mul * from->y; + to->inject_scroll_event.hscroll = -mul * from->x; + to->inject_scroll_event.vscroll = mul * from->y; return true; } diff --git a/app/src/convert.h b/app/src/convert.h index 22cf1023..5989e163 100644 --- a/app/src/convert.h +++ b/app/src/convert.h @@ -4,7 +4,7 @@ #include #include -#include "control_event.h" +#include "control_msg.h" struct complete_mouse_motion_event { SDL_MouseMotionEvent *mouse_motion_event; @@ -18,24 +18,24 @@ struct complete_mouse_wheel_event { bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, - struct control_event *to); + struct control_msg *to); bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_event *to); + struct control_msg *to); // the video size may be different from the real device size, so we need the // size to which the absolute position apply, to scale it accordingly bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_event *to); + struct control_msg *to); // on Android, a scroll event requires the current mouse position bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, struct position position, - struct control_event *to); + struct control_msg *to); #endif diff --git a/app/src/device.h b/app/src/device.h index 09934046..f3449e5e 100644 --- a/app/src/device.h +++ b/app/src/device.h @@ -11,6 +11,6 @@ // name must be at least DEVICE_NAME_FIELD_LENGTH bytes bool -device_read_info(socket_t device_socket, char *name, struct size *frame_size); +device_read_info(socket_t device_socket, char *device_name, struct size *size); #endif diff --git a/app/src/device_msg.c b/app/src/device_msg.c new file mode 100644 index 00000000..a90d78dd --- /dev/null +++ b/app/src/device_msg.c @@ -0,0 +1,48 @@ +#include "device_msg.h" + +#include +#include + +#include "buffer_util.h" +#include "log.h" + +ssize_t +device_msg_deserialize(const unsigned char *buf, size_t len, + struct device_msg *msg) { + if (len < 3) { + // at least type + empty string length + return 0; // not available + } + + msg->type = buf[0]; + switch (msg->type) { + case DEVICE_MSG_TYPE_CLIPBOARD: { + uint16_t clipboard_len = buffer_read16be(&buf[1]); + if (clipboard_len > len - 3) { + return 0; // not available + } + char *text = SDL_malloc(clipboard_len + 1); + if (!text) { + LOGW("Could not allocate text for clipboard"); + return -1; + } + if (clipboard_len) { + memcpy(text, &buf[3], clipboard_len); + } + text[clipboard_len] = '\0'; + + msg->clipboard.text = text; + return 3 + clipboard_len; + } + default: + LOGW("Unknown device message type: %d", (int) msg->type); + return -1; // error, we cannot recover + } +} + +void +device_msg_destroy(struct device_msg *msg) { + if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { + SDL_free(msg->clipboard.text); + } +} diff --git a/app/src/device_msg.h b/app/src/device_msg.h new file mode 100644 index 00000000..fd4a7eb1 --- /dev/null +++ b/app/src/device_msg.h @@ -0,0 +1,32 @@ +#ifndef DEVICEMSG_H +#define DEVICEMSG_H + +#include +#include +#include + +#define DEVICE_MSG_TEXT_MAX_LENGTH 4093 +#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH) + +enum device_msg_type { + DEVICE_MSG_TYPE_CLIPBOARD, +}; + +struct device_msg { + enum device_msg_type type; + union { + struct { + char *text; // owned, to be freed by SDL_free() + } clipboard; + }; +}; + +// return the number of bytes consumed (0 for no msg available, -1 on error) +ssize_t +device_msg_deserialize(const unsigned char *buf, size_t len, + struct device_msg *msg); + +void +device_msg_destroy(struct device_msg *msg); + +#endif diff --git a/app/src/file_handler.c b/app/src/file_handler.c index c72b598d..051db897 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -2,90 +2,22 @@ #include #include + #include "config.h" #include "command.h" #include "device.h" #include "lock_util.h" #include "log.h" -struct request { - file_handler_action_t action; - char *file; -}; - -static struct request * -request_new(file_handler_action_t action, char *file) { - struct request *req = SDL_malloc(sizeof(*req)); - if (!req) { - return NULL; - } - req->action = action; - req->file = file; - return req; -} - static void -request_free(struct request *req) { - if (!req) { - return; - } +file_handler_request_destroy(struct file_handler_request *req) { SDL_free(req->file); - SDL_free(req); -} - -static bool -request_queue_is_empty(const struct request_queue *queue) { - return queue->head == queue->tail; -} - -static bool -request_queue_is_full(const struct request_queue *queue) { - return (queue->head + 1) % REQUEST_QUEUE_SIZE == queue->tail; -} - -static bool -request_queue_init(struct request_queue *queue) { - queue->head = 0; - queue->tail = 0; - return true; -} - -static void -request_queue_destroy(struct request_queue *queue) { - int i = queue->tail; - while (i != queue->head) { - request_free(queue->reqs[i]); - i = (i + 1) % REQUEST_QUEUE_SIZE; - } -} - -static bool -request_queue_push(struct request_queue *queue, struct request *req) { - if (request_queue_is_full(queue)) { - return false; - } - queue->reqs[queue->head] = req; - queue->head = (queue->head + 1) % REQUEST_QUEUE_SIZE; - return true; -} - -static bool -request_queue_take(struct request_queue *queue, struct request **req) { - if (request_queue_is_empty(queue)) { - return false; - } - // transfer ownership - *req = queue->reqs[queue->tail]; - queue->tail = (queue->tail + 1) % REQUEST_QUEUE_SIZE; - return true; } bool file_handler_init(struct file_handler *file_handler, const char *serial) { - if (!request_queue_init(&file_handler->queue)) { - return false; - } + cbuf_init(&file_handler->queue); if (!(file_handler->mutex = SDL_CreateMutex())) { return false; @@ -100,6 +32,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial) { file_handler->serial = SDL_strdup(serial); if (!file_handler->serial) { LOGW("Cannot strdup serial"); + SDL_DestroyCond(file_handler->event_cond); SDL_DestroyMutex(file_handler->mutex); return false; } @@ -120,8 +53,12 @@ void file_handler_destroy(struct file_handler *file_handler) { SDL_DestroyCond(file_handler->event_cond); SDL_DestroyMutex(file_handler->mutex); - request_queue_destroy(&file_handler->queue); SDL_free(file_handler->serial); + + struct file_handler_request req; + while (cbuf_take(&file_handler->queue, &req)) { + file_handler_request_destroy(&req); + } } static process_t @@ -136,10 +73,7 @@ push_file(const char *serial, const char *file) { bool file_handler_request(struct file_handler *file_handler, - file_handler_action_t action, - char *file) { - bool res; - + file_handler_action_t action, char *file) { // start file_handler if it's used for the first time if (!file_handler->initialized) { if (!file_handler_start(file_handler)) { @@ -150,15 +84,14 @@ file_handler_request(struct file_handler *file_handler, LOGI("Request to %s %s", action == ACTION_INSTALL_APK ? "install" : "push", file); - struct request *req = request_new(action, file); - if (!req) { - LOGE("Could not create request"); - return false; - } + struct file_handler_request req = { + .action = action, + .file = file, + }; mutex_lock(file_handler->mutex); - bool was_empty = request_queue_is_empty(&file_handler->queue); - res = request_queue_push(&file_handler->queue, req); + bool was_empty = cbuf_is_empty(&file_handler->queue); + bool res = cbuf_push(&file_handler->queue, req); if (was_empty) { cond_signal(file_handler->event_cond); } @@ -173,8 +106,7 @@ run_file_handler(void *data) { for (;;) { mutex_lock(file_handler->mutex); file_handler->current_process = PROCESS_NONE; - while (!file_handler->stopped - && request_queue_is_empty(&file_handler->queue)) { + while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) { cond_wait(file_handler->event_cond, file_handler->mutex); } if (file_handler->stopped) { @@ -182,36 +114,36 @@ run_file_handler(void *data) { mutex_unlock(file_handler->mutex); break; } - struct request *req; - bool non_empty = request_queue_take(&file_handler->queue, &req); + struct file_handler_request req; + bool non_empty = cbuf_take(&file_handler->queue, &req); SDL_assert(non_empty); process_t process; - if (req->action == ACTION_INSTALL_APK) { - LOGI("Installing %s...", req->file); - process = install_apk(file_handler->serial, req->file); + if (req.action == ACTION_INSTALL_APK) { + LOGI("Installing %s...", req.file); + process = install_apk(file_handler->serial, req.file); } else { - LOGI("Pushing %s...", req->file); - process = push_file(file_handler->serial, req->file); + LOGI("Pushing %s...", req.file); + process = push_file(file_handler->serial, req.file); } file_handler->current_process = process; mutex_unlock(file_handler->mutex); - if (req->action == ACTION_INSTALL_APK) { + if (req.action == ACTION_INSTALL_APK) { if (process_check_success(process, "adb install")) { - LOGI("%s successfully installed", req->file); + LOGI("%s successfully installed", req.file); } else { - LOGE("Failed to install %s", req->file); + LOGE("Failed to install %s", req.file); } } else { if (process_check_success(process, "adb push")) { - LOGI("%s successfully pushed to /sdcard/", req->file); + LOGI("%s successfully pushed to /sdcard/", req.file); } else { - LOGE("Failed to push %s to /sdcard/", req->file); + LOGE("Failed to push %s to /sdcard/", req.file); } } - request_free(req); + file_handler_request_destroy(&req); } return 0; } diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 382477d8..22245105 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -5,21 +5,21 @@ #include #include +#include "cbuf.h" #include "command.h" -#define REQUEST_QUEUE_SIZE 16 - typedef enum { ACTION_INSTALL_APK, ACTION_PUSH_FILE, } file_handler_action_t; -struct request_queue { - struct request *reqs[REQUEST_QUEUE_SIZE]; - int tail; - int head; +struct file_handler_request { + file_handler_action_t action; + char *file; }; +struct file_handler_request_queue CBUF(struct file_handler_request, 16); + struct file_handler { char *serial; SDL_Thread *thread; @@ -28,7 +28,7 @@ struct file_handler { bool stopped; bool initialized; process_t current_process; - struct request_queue queue; + struct file_handler_request_queue queue; }; bool diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 6c8ef795..daece470 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -1,70 +1,169 @@ #include "fps_counter.h" +#include #include +#include "lock_util.h" #include "log.h" -void +#define FPS_COUNTER_INTERVAL_MS 1000 + +bool fps_counter_init(struct fps_counter *counter) { - counter->started = false; - // no need to initialize the other fields, they are meaningful only when - // started is true + counter->mutex = SDL_CreateMutex(); + if (!counter->mutex) { + return false; + } + + counter->state_cond = SDL_CreateCond(); + if (!counter->state_cond) { + SDL_DestroyMutex(counter->mutex); + return false; + } + + counter->thread = NULL; + SDL_AtomicSet(&counter->started, 0); + // no need to initialize the other fields, they are unused until started + + return true; } void -fps_counter_start(struct fps_counter *counter) { - counter->started = true; - counter->slice_start = SDL_GetTicks(); +fps_counter_destroy(struct fps_counter *counter) { + SDL_DestroyCond(counter->state_cond); + SDL_DestroyMutex(counter->mutex); +} + +// must be called with mutex locked +static void +display_fps(struct fps_counter *counter) { + unsigned rendered_per_second = + counter->nr_rendered * 1000 / FPS_COUNTER_INTERVAL_MS; + if (counter->nr_skipped) { + LOGI("%u fps (+%u frames skipped)", rendered_per_second, + counter->nr_skipped); + } else { + LOGI("%u fps", rendered_per_second); + } +} + +// must be called with mutex locked +static void +check_interval_expired(struct fps_counter *counter, uint32_t now) { + if (now < counter->next_timestamp) { + return; + } + + display_fps(counter); counter->nr_rendered = 0; -#ifdef SKIP_FRAMES counter->nr_skipped = 0; -#endif + // add a multiple of the interval + uint32_t elapsed_slices = + (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL_MS + 1; + counter->next_timestamp += FPS_COUNTER_INTERVAL_MS * elapsed_slices; +} + +static int +run_fps_counter(void *data) { + struct fps_counter *counter = data; + + mutex_lock(counter->mutex); + while (!counter->interrupted) { + while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) { + cond_wait(counter->state_cond, counter->mutex); + } + while (!counter->interrupted && SDL_AtomicGet(&counter->started)) { + uint32_t now = SDL_GetTicks(); + check_interval_expired(counter, now); + + SDL_assert(counter->next_timestamp > now); + uint32_t remaining = counter->next_timestamp - now; + + // ignore the reason (timeout or signaled), we just loop anyway + cond_wait_timeout(counter->state_cond, counter->mutex, remaining); + } + } + mutex_unlock(counter->mutex); + return 0; +} + +bool +fps_counter_start(struct fps_counter *counter) { + mutex_lock(counter->mutex); + counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS; + counter->nr_rendered = 0; + counter->nr_skipped = 0; + mutex_unlock(counter->mutex); + + SDL_AtomicSet(&counter->started, 1); + cond_signal(counter->state_cond); + + // counter->thread is always accessed from the same thread, no need to lock + if (!counter->thread) { + counter->thread = + SDL_CreateThread(run_fps_counter, "fps counter", counter); + if (!counter->thread) { + LOGE("Could not start FPS counter thread"); + return false; + } + } + + return true; } void fps_counter_stop(struct fps_counter *counter) { - counter->started = false; + SDL_AtomicSet(&counter->started, 0); + cond_signal(counter->state_cond); } -static void -display_fps(struct fps_counter *counter) { -#ifdef SKIP_FRAMES - if (counter->nr_skipped) { - LOGI("%d fps (+%d frames skipped)", counter->nr_rendered, - counter->nr_skipped); - } else { -#endif - LOGI("%d fps", counter->nr_rendered); -#ifdef SKIP_FRAMES +bool +fps_counter_is_started(struct fps_counter *counter) { + return SDL_AtomicGet(&counter->started); +} + +void +fps_counter_interrupt(struct fps_counter *counter) { + if (!counter->thread) { + return; } -#endif + + mutex_lock(counter->mutex); + counter->interrupted = true; + mutex_unlock(counter->mutex); + // wake up blocking wait + cond_signal(counter->state_cond); } -static void -check_expired(struct fps_counter *counter) { - uint32_t now = SDL_GetTicks(); - if (now - counter->slice_start >= 1000) { - display_fps(counter); - // add a multiple of one second - uint32_t elapsed_slices = (now - counter->slice_start) / 1000; - counter->slice_start += 1000 * elapsed_slices; - counter->nr_rendered = 0; -#ifdef SKIP_FRAMES - counter->nr_skipped = 0; -#endif +void +fps_counter_join(struct fps_counter *counter) { + if (counter->thread) { + SDL_WaitThread(counter->thread, NULL); } } void fps_counter_add_rendered_frame(struct fps_counter *counter) { - check_expired(counter); + if (!SDL_AtomicGet(&counter->started)) { + return; + } + + mutex_lock(counter->mutex); + uint32_t now = SDL_GetTicks(); + check_interval_expired(counter, now); ++counter->nr_rendered; + mutex_unlock(counter->mutex); } -#ifdef SKIP_FRAMES void fps_counter_add_skipped_frame(struct fps_counter *counter) { - check_expired(counter); + if (!SDL_AtomicGet(&counter->started)) { + return; + } + + mutex_lock(counter->mutex); + uint32_t now = SDL_GetTicks(); + check_interval_expired(counter, now); ++counter->nr_skipped; + mutex_unlock(counter->mutex); } -#endif diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index dcdf10bf..6b560a35 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -3,33 +3,53 @@ #include #include - -#include "config.h" +#include +#include +#include struct fps_counter { - bool started; - uint32_t slice_start; // initialized by SDL_GetTicks() - int nr_rendered; -#ifdef SKIP_FRAMES - int nr_skipped; -#endif + SDL_Thread *thread; + SDL_mutex *mutex; + SDL_cond *state_cond; + + // atomic so that we can check without locking the mutex + // if the FPS counter is disabled, we don't want to lock unnecessarily + SDL_atomic_t started; + + // the following fields are protected by the mutex + bool interrupted; + unsigned nr_rendered; + unsigned nr_skipped; + uint32_t next_timestamp; }; -void +bool fps_counter_init(struct fps_counter *counter); void +fps_counter_destroy(struct fps_counter *counter); + +bool fps_counter_start(struct fps_counter *counter); void fps_counter_stop(struct fps_counter *counter); +bool +fps_counter_is_started(struct fps_counter *counter); + +// request to stop the thread (on quit) +// must be called before fps_counter_join() +void +fps_counter_interrupt(struct fps_counter *counter); + +void +fps_counter_join(struct fps_counter *counter); + void fps_counter_add_rendered_frame(struct fps_counter *counter); -#ifdef SKIP_FRAMES void fps_counter_add_skipped_frame(struct fps_counter *counter); -#endif #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index f77d2d26..fb8ef8f0 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -39,23 +39,23 @@ static void send_keycode(struct controller *controller, enum android_keycode keycode, int actions, const char *name) { // send DOWN event - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_KEYCODE; - control_event.keycode_event.keycode = keycode; - control_event.keycode_event.metastate = 0; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + msg.inject_keycode.keycode = keycode; + msg.inject_keycode.metastate = 0; if (actions & ACTION_DOWN) { - control_event.keycode_event.action = AKEY_EVENT_ACTION_DOWN; - if (!controller_push_event(controller, &control_event)) { - LOGW("Cannot send %s (DOWN)", name); + msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'inject %s (DOWN)'", name); return; } } if (actions & ACTION_UP) { - control_event.keycode_event.action = AKEY_EVENT_ACTION_UP; - if (!controller_push_event(controller, &control_event)) { - LOGW("Cannot send %s (UP)", name); + msg.inject_keycode.action = AKEY_EVENT_ACTION_UP; + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'inject %s (UP)'", name); } } } @@ -98,51 +98,93 @@ action_menu(struct controller *controller, int actions) { // turn the screen on if it was off, press BACK otherwise static void press_back_or_turn_screen_on(struct controller *controller) { - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_COMMAND; - control_event.command_event.action = - CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; - if (!controller_push_event(controller, &control_event)) { - LOGW("Cannot turn screen on"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'turn screen on'"); } } static void expand_notification_panel(struct controller *controller) { - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_COMMAND; - control_event.command_event.action = - CONTROL_EVENT_COMMAND_EXPAND_NOTIFICATION_PANEL; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; - if (!controller_push_event(controller, &control_event)) { - LOGW("Cannot expand notification panel"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'expand notification panel'"); } } static void collapse_notification_panel(struct controller *controller) { - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_COMMAND; - control_event.command_event.action = - CONTROL_EVENT_COMMAND_COLLAPSE_NOTIFICATION_PANEL; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL; - if (!controller_push_event(controller, &control_event)) { - LOGW("Cannot collapse notification panel"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'collapse notification panel'"); } } static void -switch_fps_counter_state(struct video_buffer *vb) { - mutex_lock(vb->mutex); - if (vb->fps_counter.started) { - LOGI("FPS counter stopped"); - fps_counter_stop(&vb->fps_counter); - } else { - LOGI("FPS counter started"); - fps_counter_start(&vb->fps_counter); +request_device_clipboard(struct controller *controller) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request device clipboard"); + } +} + +static void +set_device_clipboard(struct controller *controller) { + char *text = SDL_GetClipboardText(); + if (!text) { + LOGW("Cannot get clipboard text: %s", SDL_GetError()); + return; + } + if (!*text) { + // empty text + SDL_free(text); + return; + } + + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; + msg.set_clipboard.text = text; + + if (!controller_push_msg(controller, &msg)) { + SDL_free(text); + LOGW("Cannot request 'set device clipboard'"); + } +} + +static void +set_screen_power_mode(struct controller *controller, + enum screen_power_mode mode) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = mode; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'set screen power mode'"); + } +} + +static void +switch_fps_counter_state(struct fps_counter *fps_counter) { + // the started state can only be written from the current thread, so there + // is no ToCToU issue + if (fps_counter_is_started(fps_counter)) { + fps_counter_stop(fps_counter); + LOGI("FPS counter stopped"); + } else { + if (fps_counter_start(fps_counter)) { + LOGI("FPS counter started"); + } else { + LOGE("FPS counter starting failed"); + } } - mutex_unlock(vb->mutex); } static void @@ -158,12 +200,12 @@ clipboard_paste(struct controller *controller) { return; } - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_TEXT; - control_event.text_event.text = text; - if (!controller_push_event(controller, &control_event)) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + msg.inject_text.text = text; + if (!controller_push_msg(controller, &msg)) { SDL_free(text); - LOGW("Cannot send clipboard paste event"); + LOGW("Cannot request 'paste clipboard'"); } } @@ -176,16 +218,16 @@ input_manager_process_text_input(struct input_manager *input_manager, // letters and space are handled as raw key event return; } - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_TEXT; - control_event.text_event.text = SDL_strdup(event->text); - if (!control_event.text_event.text) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + msg.inject_text.text = SDL_strdup(event->text); + if (!msg.inject_text.text) { LOGW("Cannot strdup input text"); return; } - if (!controller_push_event(input_manager->controller, &control_event)) { - SDL_free(control_event.text_event.text); - LOGW("Cannot send text event"); + if (!controller_push_msg(input_manager->controller, &msg)) { + SDL_free(msg.inject_text.text); + LOGW("Cannot request 'inject text'"); } } @@ -193,6 +235,9 @@ void input_manager_process_key(struct input_manager *input_manager, const SDL_KeyboardEvent *event, bool control) { + // control: indicates the state of the command-line option --no-control + // ctrl: the Ctrl key + bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL); bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT); bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI); @@ -203,37 +248,45 @@ input_manager_process_key(struct input_manager *input_manager, return; } + struct controller *controller = input_manager->controller; + // capture all Ctrl events if (ctrl | meta) { SDL_Keycode keycode = event->keysym.sym; - int action = event->type == SDL_KEYDOWN ? ACTION_DOWN : ACTION_UP; + bool down = event->type == SDL_KEYDOWN; + int action = down ? ACTION_DOWN : ACTION_UP; bool repeat = event->repeat; bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT); switch (keycode) { case SDLK_h: if (control && ctrl && !meta && !shift && !repeat) { - action_home(input_manager->controller, action); + action_home(controller, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: if (control && ctrl && !meta && !shift && !repeat) { - action_back(input_manager->controller, action); + action_back(controller, action); } return; case SDLK_s: if (control && ctrl && !meta && !shift && !repeat) { - action_app_switch(input_manager->controller, action); + action_app_switch(controller, action); } return; case SDLK_m: if (control && ctrl && !meta && !shift && !repeat) { - action_menu(input_manager->controller, action); + action_menu(controller, action); } return; case SDLK_p: if (control && ctrl && !meta && !shift && !repeat) { - action_power(input_manager->controller, action); + action_power(controller, action); + } + return; + case SDLK_o: + if (control && ctrl && !shift && !meta && down) { + set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF); } return; case SDLK_DOWN: @@ -243,7 +296,7 @@ input_manager_process_key(struct input_manager *input_manager, if (control && ctrl && !meta && !shift) { #endif // forward repeated events - action_volume_down(input_manager->controller, action); + action_volume_down(controller, action); } return; case SDLK_UP: @@ -253,46 +306,53 @@ input_manager_process_key(struct input_manager *input_manager, if (control && ctrl && !meta && !shift) { #endif // forward repeated events - action_volume_up(input_manager->controller, action); + action_volume_up(controller, action); + } + return; + case SDLK_c: + if (control && ctrl && !meta && !shift && !repeat && down) { + request_device_clipboard(controller); } return; case SDLK_v: - if (control && ctrl && !meta && !shift && !repeat - && event->type == SDL_KEYDOWN) { - clipboard_paste(input_manager->controller); + if (control && ctrl && !meta && !repeat && down) { + if (shift) { + // store the text in the device clipboard + set_device_clipboard(controller); + } else { + // inject the text as input events + clipboard_paste(controller); + } } return; case SDLK_f: - if (ctrl && !meta && !shift && !repeat - && event->type == SDL_KEYDOWN) { + if (ctrl && !meta && !shift && !repeat && down) { screen_switch_fullscreen(input_manager->screen); } return; case SDLK_x: - if (ctrl && !meta && !shift && !repeat - && event->type == SDL_KEYDOWN) { + if (ctrl && !meta && !shift && !repeat && down) { screen_resize_to_fit(input_manager->screen); } return; case SDLK_g: - if (ctrl && !meta && !shift && !repeat - && event->type == SDL_KEYDOWN) { + if (ctrl && !meta && !shift && !repeat && down) { screen_resize_to_pixel_perfect(input_manager->screen); } return; case SDLK_i: - if (ctrl && !meta && !shift && !repeat - && event->type == SDL_KEYDOWN) { - switch_fps_counter_state(input_manager->video_buffer); + if (ctrl && !meta && !shift && !repeat && down) { + struct fps_counter *fps_counter = + input_manager->video_buffer->fps_counter; + switch_fps_counter_state(fps_counter); } return; case SDLK_n: - if (control && ctrl && !meta - && !repeat && event->type == SDL_KEYDOWN) { + if (control && ctrl && !meta && !repeat && down) { if (shift) { - collapse_notification_panel(input_manager->controller); + collapse_notification_panel(controller); } else { - expand_notification_panel(input_manager->controller); + expand_notification_panel(controller); } } return; @@ -305,10 +365,10 @@ input_manager_process_key(struct input_manager *input_manager, return; } - struct control_event control_event; - if (input_key_from_sdl_to_android(event, &control_event)) { - if (!controller_push_event(input_manager->controller, &control_event)) { - LOGW("Cannot send control event"); + struct control_msg msg; + if (input_key_from_sdl_to_android(event, &msg)) { + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'inject keycode'"); } } } @@ -320,12 +380,12 @@ input_manager_process_mouse_motion(struct input_manager *input_manager, // do not send motion events when no button is pressed return; } - struct control_event control_event; + struct control_msg msg; if (mouse_motion_from_sdl_to_android(event, input_manager->screen->frame_size, - &control_event)) { - if (!controller_push_event(input_manager->controller, &control_event)) { - LOGW("Cannot send mouse motion event"); + &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Cannot request 'inject mouse motion event'"); } } } @@ -352,9 +412,8 @@ input_manager_process_mouse_button(struct input_manager *input_manager, } // double-click on black borders resize to fit the device screen if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { - bool outside = is_outside_device_screen(input_manager, - event->x, - event->y); + bool outside = + is_outside_device_screen(input_manager, event->x, event->y); if (outside) { screen_resize_to_fit(input_manager->screen); return; @@ -367,12 +426,12 @@ input_manager_process_mouse_button(struct input_manager *input_manager, return; } - struct control_event control_event; + struct control_msg msg; if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, - &control_event)) { - if (!controller_push_event(input_manager->controller, &control_event)) { - LOGW("Cannot send mouse button event"); + &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Cannot request 'inject mouse button event'"); } } } @@ -384,10 +443,10 @@ input_manager_process_mouse_wheel(struct input_manager *input_manager, .screen_size = input_manager->screen->frame_size, .point = get_mouse_point(input_manager->screen), }; - struct control_event control_event; - if (mouse_wheel_from_sdl_to_android(event, position, &control_event)) { - if (!controller_push_event(input_manager->controller, &control_event)) { - LOGW("Cannot send mouse wheel event"); + struct control_msg msg; + if (mouse_wheel_from_sdl_to_android(event, position, &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Cannot request 'inject mouse wheel event'"); } } } diff --git a/app/src/lock_util.c b/app/src/lock_util.c deleted file mode 100644 index 7b70ba6b..00000000 --- a/app/src/lock_util.c +++ /dev/null @@ -1,38 +0,0 @@ -#include -#include -#include - -#include "log.h" - -void -mutex_lock(SDL_mutex *mutex) { - if (SDL_LockMutex(mutex)) { - LOGC("Could not lock mutex"); - abort(); - } -} - -void -mutex_unlock(SDL_mutex *mutex) { - if (SDL_UnlockMutex(mutex)) { - LOGC("Could not unlock mutex"); - abort(); - } -} - -void -cond_wait(SDL_cond *cond, SDL_mutex *mutex) { - if (SDL_CondWait(cond, mutex)) { - LOGC("Could not wait on condition"); - abort(); - } -} - -void -cond_signal(SDL_cond *cond) { - if (SDL_CondSignal(cond)) { - LOGC("Could not signal a condition"); - abort(); - } -} - diff --git a/app/src/lock_util.h b/app/src/lock_util.h index 99c1f8d6..d1ca7336 100644 --- a/app/src/lock_util.h +++ b/app/src/lock_util.h @@ -1,20 +1,51 @@ #ifndef LOCKUTIL_H #define LOCKUTIL_H -// forward declarations -typedef struct SDL_mutex SDL_mutex; -typedef struct SDL_cond SDL_cond; +#include +#include -void -mutex_lock(SDL_mutex *mutex); +#include "log.h" -void -mutex_unlock(SDL_mutex *mutex); +static inline void +mutex_lock(SDL_mutex *mutex) { + if (SDL_LockMutex(mutex)) { + LOGC("Could not lock mutex"); + abort(); + } +} -void -cond_wait(SDL_cond *cond, SDL_mutex *mutex); +static inline void +mutex_unlock(SDL_mutex *mutex) { + if (SDL_UnlockMutex(mutex)) { + LOGC("Could not unlock mutex"); + abort(); + } +} -void -cond_signal(SDL_cond *cond); +static inline void +cond_wait(SDL_cond *cond, SDL_mutex *mutex) { + if (SDL_CondWait(cond, mutex)) { + LOGC("Could not wait on condition"); + abort(); + } +} + +static inline int +cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) { + int r = SDL_CondWaitTimeout(cond, mutex, ms); + if (r < 0) { + LOGC("Could not wait on condition with timeout"); + abort(); + } + return r; +} + +static inline void +cond_signal(SDL_cond *cond) { + if (SDL_CondSignal(cond)) { + LOGC("Could not signal a condition"); + abort(); + } +} #endif diff --git a/app/src/main.c b/app/src/main.c index fe93673f..bf3b7a50 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -28,6 +28,8 @@ struct args { uint16_t max_size; uint32_t bit_rate; bool always_on_top; + bool turn_screen_off; + bool render_expired_frames; }; static void usage(const char *arg0) { @@ -78,10 +80,19 @@ static void usage(const char *arg0) { " The format is determined by the -F/--record-format option if\n" " set, or by the file extension (.mp4 or .mkv).\n" "\n" + " --render-expired-frames\n" + " By default, to minimize latency, scrcpy always renders the\n" + " last available decoded frame, and drops any previous ones.\n" + " This flag forces to render all frames, at a cost of a\n" + " possible increased latency.\n" + "\n" " -s, --serial\n" " The device serial number. Mandatory only if several devices\n" " are connected to adb.\n" "\n" + " -S, --turn-screen-off\n" + " Turn the device screen off immediately.\n" + "\n" " -t, --show-touches\n" " Enable \"show touches\" on start, disable on quit.\n" " It only shows physical touches (not clicks from scrcpy).\n" @@ -129,7 +140,10 @@ static void usage(const char *arg0) { " click on POWER (turn screen on/off)\n" "\n" " Right-click (when screen is off)\n" - " turn screen on\n" + " power on\n" + "\n" + " Ctrl+o\n" + " turn device screen off (keep mirroring)\n" "\n" " Ctrl+n\n" " expand notification panel\n" @@ -137,9 +151,15 @@ static void usage(const char *arg0) { " Ctrl+Shift+n\n" " collapse notification panel\n" "\n" + " Ctrl+c\n" + " copy device clipboard to computer\n" + "\n" " Ctrl+v\n" " paste computer clipboard to device\n" "\n" + " Ctrl+Shift+v\n" + " copy computer clipboard to device\n" + "\n" " Ctrl+i\n" " enable/disable FPS counter (print frames/second in logs)\n" "\n" @@ -274,27 +294,32 @@ guess_record_format(const char *filename) { return 0; } +#define OPT_RENDER_EXPIRED_FRAMES 1000 + static bool parse_args(struct args *args, int argc, char *argv[]) { static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, 'T'}, - {"bit-rate", required_argument, NULL, 'b'}, - {"crop", required_argument, NULL, 'c'}, - {"fullscreen", no_argument, NULL, 'f'}, - {"help", no_argument, NULL, 'h'}, - {"max-size", required_argument, NULL, 'm'}, - {"no-control", no_argument, NULL, 'n'}, - {"no-display", no_argument, NULL, 'N'}, - {"port", required_argument, NULL, 'p'}, - {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, 'f'}, - {"serial", required_argument, NULL, 's'}, - {"show-touches", no_argument, NULL, 't'}, - {"version", no_argument, NULL, 'v'}, - {NULL, 0, NULL, 0 }, + {"always-on-top", no_argument, NULL, 'T'}, + {"bit-rate", required_argument, NULL, 'b'}, + {"crop", required_argument, NULL, 'c'}, + {"fullscreen", no_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {"max-size", required_argument, NULL, 'm'}, + {"no-control", no_argument, NULL, 'n'}, + {"no-display", no_argument, NULL, 'N'}, + {"port", required_argument, NULL, 'p'}, + {"record", required_argument, NULL, 'r'}, + {"record-format", required_argument, NULL, 'f'}, + {"render-expired-frames", no_argument, NULL, + OPT_RENDER_EXPIRED_FRAMES}, + {"serial", required_argument, NULL, 's'}, + {"show-touches", no_argument, NULL, 't'}, + {"turn-screen-off", no_argument, NULL, 'S'}, + {"version", no_argument, NULL, 'v'}, + {NULL, 0, NULL, 0 }, }; int c; - while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:tTv", long_options, + while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -338,6 +363,9 @@ parse_args(struct args *args, int argc, char *argv[]) { case 's': args->serial = optarg; break; + case 'S': + args->turn_screen_off = true; + break; case 't': args->show_touches = true; break; @@ -347,6 +375,9 @@ parse_args(struct args *args, int argc, char *argv[]) { case 'v': args->version = true; break; + case OPT_RENDER_EXPIRED_FRAMES: + args->render_expired_frames = true; + break; default: // getopt prints the error message on stderr return false; @@ -408,6 +439,8 @@ main(int argc, char *argv[]) { .always_on_top = false, .no_control = false, .no_display = false, + .turn_screen_off = false, + .render_expired_frames = false, }; if (!parse_args(&args, argc, argv)) { return 1; @@ -446,8 +479,10 @@ main(int argc, char *argv[]) { .show_touches = args.show_touches, .fullscreen = args.fullscreen, .always_on_top = args.always_on_top, - .no_control = args.no_control, - .no_display = args.no_display, + .control = !args.no_control, + .display = !args.no_display, + .turn_screen_off = args.turn_screen_off, + .render_expired_frames = args.render_expired_frames, }; int res = scrcpy(&options) ? 0 : 1; diff --git a/app/src/net.c b/app/src/net.c index b5b227c2..a0bc38f2 100644 --- a/app/src/net.c +++ b/app/src/net.c @@ -33,6 +33,7 @@ net_connect(uint32_t addr, uint16_t port) { if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { perror("connect"); + net_close(sock); return INVALID_SOCKET; } @@ -60,11 +61,13 @@ net_listen(uint32_t addr, uint16_t port, int backlog) { if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { perror("bind"); + net_close(sock); return INVALID_SOCKET; } if (listen(sock, backlog) == SOCKET_ERROR) { perror("listen"); + net_close(sock); return INVALID_SOCKET; } diff --git a/app/src/receiver.c b/app/src/receiver.c new file mode 100644 index 00000000..1c80bb00 --- /dev/null +++ b/app/src/receiver.c @@ -0,0 +1,107 @@ +#include "receiver.h" + +#include +#include + +#include "config.h" +#include "device_msg.h" +#include "lock_util.h" +#include "log.h" + +bool +receiver_init(struct receiver *receiver, socket_t control_socket) { + if (!(receiver->mutex = SDL_CreateMutex())) { + return false; + } + receiver->control_socket = control_socket; + return true; +} + +void +receiver_destroy(struct receiver *receiver) { + SDL_DestroyMutex(receiver->mutex); +} + +static void +process_msg(struct receiver *receiver, struct device_msg *msg) { + switch (msg->type) { + case DEVICE_MSG_TYPE_CLIPBOARD: + LOGI("Device clipboard copied"); + SDL_SetClipboardText(msg->clipboard.text); + break; + } +} + +static ssize_t +process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { + size_t head = 0; + for (;;) { + struct device_msg msg; + ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg); + if (r == -1) { + return -1; + } + if (r == 0) { + return head; + } + + process_msg(receiver, &msg); + device_msg_destroy(&msg); + + head += r; + SDL_assert(head <= len); + if (head == len) { + return head; + } + } +} + +static int +run_receiver(void *data) { + struct receiver *receiver = data; + + unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE]; + size_t head = 0; + + for (;;) { + SDL_assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); + ssize_t r = net_recv(receiver->control_socket, buf, + DEVICE_MSG_SERIALIZED_MAX_SIZE - head); + if (r <= 0) { + LOGD("Receiver stopped"); + break; + } + + ssize_t consumed = process_msgs(receiver, buf, r); + if (consumed == -1) { + // an error occurred + break; + } + + if (consumed) { + // shift the remaining data in the buffer + memmove(buf, &buf[consumed], r - consumed); + head = r - consumed; + } + } + + return 0; +} + +bool +receiver_start(struct receiver *receiver) { + LOGD("Starting receiver thread"); + + receiver->thread = SDL_CreateThread(run_receiver, "receiver", receiver); + if (!receiver->thread) { + LOGC("Could not start receiver thread"); + return false; + } + + return true; +} + +void +receiver_join(struct receiver *receiver) { + SDL_WaitThread(receiver->thread, NULL); +} diff --git a/app/src/receiver.h b/app/src/receiver.h new file mode 100644 index 00000000..c119b827 --- /dev/null +++ b/app/src/receiver.h @@ -0,0 +1,32 @@ +#ifndef RECEIVER_H +#define RECEIVER_H + +#include +#include +#include + +#include "net.h" + +// receive events from the device +// managed by the controller +struct receiver { + socket_t control_socket; + SDL_Thread *thread; + SDL_mutex *mutex; +}; + +bool +receiver_init(struct receiver *receiver, socket_t control_socket); + +void +receiver_destroy(struct receiver *receiver); + +bool +receiver_start(struct receiver *receiver); + +// no receiver_stop(), it will automatically stop on control_socket shutdown + +void +receiver_join(struct receiver *receiver); + +#endif diff --git a/app/src/recorder.h b/app/src/recorder.h index 26c4a3c3..8a8e3310 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -20,7 +20,7 @@ struct recorder { }; bool -recorder_init(struct recorder *recoder, const char *filename, +recorder_init(struct recorder *recorder, const char *filename, enum recorder_format format, struct size declared_frame_size); void diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b777b770..761edb69 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -29,6 +29,7 @@ static struct server server = SERVER_INITIALIZER; static struct screen screen = SCREEN_INITIALIZER; +static struct fps_counter fps_counter; static struct video_buffer video_buffer; static struct stream stream; static struct decoder decoder; @@ -76,6 +77,11 @@ sdl_init_and_configure(bool display) { } #endif + // Do not minimize on focus loss + if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) { + LOGW("Could not disable minimize on focus loss"); + } + // Do not disable the screensaver when scrcpy is running SDL_EnableScreenSaver(); @@ -132,7 +138,7 @@ handle_event(SDL_Event *event, bool control) { screen_show_window(&screen); } if (!screen_update_frame(&screen, &video_buffer)) { - return false; + return EVENT_RESULT_CONTINUE; } break; case SDL_WINDOWEVENT: @@ -266,9 +272,15 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { bool scrcpy(const struct scrcpy_options *options) { bool record = !!options->record_filename; - if (!server_start(&server, options->serial, options->port, - options->max_size, options->bit_rate, options->crop, - record)) { + struct server_params params = { + .crop = options->crop, + .local_port = options->port, + .max_size = options->max_size, + .bit_rate = options->bit_rate, + .send_frame_meta = record, + .control = options->control, + }; + if (!server_start(&server, options->serial, ¶ms)) { return false; } @@ -280,21 +292,22 @@ scrcpy(const struct scrcpy_options *options) { show_touches_waited = false; } - bool ret = true; + bool ret = false; - bool display = !options->no_display; - bool control = !options->no_control; + bool fps_counter_initialized = false; + bool video_buffer_initialized = false; + bool file_handler_initialized = false; + bool recorder_initialized = false; + bool stream_started = false; + bool controller_initialized = false; + bool controller_started = false; - if (!sdl_init_and_configure(display)) { - ret = false; - goto finally_destroy_server; + if (!sdl_init_and_configure(options->display)) { + goto end; } - socket_t device_socket = server_connect_to(&server); - if (device_socket == INVALID_SOCKET) { - server_stop(&server); - ret = false; - goto finally_destroy_server; + if (!server_connect_to(&server)) { + goto end; } char device_name[DEVICE_NAME_FIELD_LENGTH]; @@ -303,24 +316,28 @@ scrcpy(const struct scrcpy_options *options) { // screenrecord does not send frames when the screen content does not // change therefore, we transmit the screen size before the video stream, // to be able to init the window immediately - if (!device_read_info(device_socket, device_name, &frame_size)) { - server_stop(&server); - ret = false; - goto finally_destroy_server; + if (!device_read_info(server.video_socket, device_name, &frame_size)) { + goto end; } struct decoder *dec = NULL; - if (display) { - if (!video_buffer_init(&video_buffer)) { - server_stop(&server); - ret = false; - goto finally_destroy_server; + if (options->display) { + if (!fps_counter_init(&fps_counter)) { + goto end; } + fps_counter_initialized = true; - if (control && !file_handler_init(&file_handler, server.serial)) { - ret = false; - server_stop(&server); - goto finally_destroy_video_buffer; + if (!video_buffer_init(&video_buffer, &fps_counter, + options->render_expired_frames)) { + goto end; + } + video_buffer_initialized = true; + + if (options->control) { + if (!file_handler_init(&file_handler, server.serial)) { + goto end; + } + file_handler_initialized = true; } decoder_init(&decoder, &video_buffer); @@ -333,42 +350,49 @@ scrcpy(const struct scrcpy_options *options) { options->record_filename, options->record_format, frame_size)) { - ret = false; - server_stop(&server); - goto finally_destroy_file_handler; + goto end; } rec = &recorder; + recorder_initialized = true; } av_log_set_callback(av_log_callback); - stream_init(&stream, device_socket, dec, rec); + stream_init(&stream, server.video_socket, dec, rec); // now we consumed the header values, the socket receives the video stream // start the stream if (!stream_start(&stream)) { - ret = false; - server_stop(&server); - goto finally_destroy_recorder; + goto end; } + stream_started = true; - if (display) { - if (control) { - if (!controller_init(&controller, device_socket)) { - ret = false; - goto finally_stop_stream; + if (options->display) { + if (options->control) { + if (!controller_init(&controller, server.control_socket)) { + goto end; } + controller_initialized = true; if (!controller_start(&controller)) { - ret = false; - goto finally_destroy_controller; + goto end; } + controller_started = true; } if (!screen_init_rendering(&screen, device_name, frame_size, options->always_on_top)) { - ret = false; - goto finally_stop_and_join_controller; + goto end; + } + + if (options->turn_screen_off) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; + + if (!controller_push_msg(&controller, &msg)) { + LOGW("Cannot request 'set screen power mode'"); + } } if (options->fullscreen) { @@ -381,48 +405,67 @@ scrcpy(const struct scrcpy_options *options) { show_touches_waited = true; } - ret = event_loop(display, control); + ret = event_loop(options->display, options->control); LOGD("quit..."); screen_destroy(&screen); -finally_stop_and_join_controller: - if (display && control) { +end: + // stop stream and controller so that they don't continue once their socket + // is shutdown + if (stream_started) { + stream_stop(&stream); + } + if (controller_started) { controller_stop(&controller); + } + if (file_handler_initialized) { + file_handler_stop(&file_handler); + } + if (fps_counter_initialized) { + fps_counter_interrupt(&fps_counter); + } + + // shutdown the sockets and kill the server + server_stop(&server); + + // now that the sockets are shutdown, the stream and controller are + // interrupted, we can join them + if (stream_started) { + stream_join(&stream); + } + if (controller_started) { controller_join(&controller); } -finally_destroy_controller: - if (display && control) { + if (controller_initialized) { controller_destroy(&controller); } -finally_stop_stream: - stream_stop(&stream); - // stop the server before stream_join() to wake up the stream - server_stop(&server); - stream_join(&stream); -finally_destroy_recorder: - if (record) { + + if (recorder_initialized) { recorder_destroy(&recorder); } -finally_destroy_file_handler: - if (display && control) { - file_handler_stop(&file_handler); + + if (file_handler_initialized) { file_handler_join(&file_handler); file_handler_destroy(&file_handler); } -finally_destroy_video_buffer: - if (display) { + + if (video_buffer_initialized) { video_buffer_destroy(&video_buffer); } -finally_destroy_server: + + if (fps_counter_initialized) { + fps_counter_join(&fps_counter); + fps_counter_destroy(&fps_counter); + } + if (options->show_touches) { if (!show_touches_waited) { // wait the process which enabled "show touches" wait_show_touches(proc_show_touches); } LOGI("Disable show_touches"); - proc_show_touches = set_show_touches_enabled(options->serial, - false); + proc_show_touches = set_show_touches_enabled(options->serial, false); wait_show_touches(proc_show_touches); } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 114c12a4..d705d2db 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -16,8 +16,10 @@ struct scrcpy_options { bool show_touches; bool fullscreen; bool always_on_top; - bool no_control; - bool no_display; + bool control; + bool display; + bool turn_screen_off; + bool render_expired_frames; }; bool diff --git a/app/src/server.c b/app/src/server.c index c37e0070..d0599bef 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -2,31 +2,67 @@ #include #include +#include #include #include #include #include "config.h" +#include "command.h" #include "log.h" #include "net.h" #define SOCKET_NAME "scrcpy" +#define SERVER_FILENAME "scrcpy-server.jar" -#ifdef OVERRIDE_SERVER_PATH -# define DEFAULT_SERVER_PATH OVERRIDE_SERVER_PATH -#else -# define DEFAULT_SERVER_PATH PREFIX PREFIXED_SERVER_PATH -#endif - -#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" +#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FLENAME +#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME static const char * get_server_path(void) { - const char *server_path = getenv("SCRCPY_SERVER_PATH"); - if (!server_path) { - server_path = DEFAULT_SERVER_PATH; + const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); + if (server_path_env) { + LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env); + // if the envvar is set, use it + return server_path_env; } + +#ifndef PORTABLE + LOGD("Using server: " DEFAULT_SERVER_PATH); + // the absolute path is hardcoded + return DEFAULT_SERVER_PATH; +#else + // use scrcpy-server.jar in the same directory as the executable + char *executable_path = get_executable_path(); + if (!executable_path) { + LOGE("Cannot get executable path, " + "using " SERVER_FILENAME " from current directory"); + // not found, use current directory + return SERVER_FILENAME; + } + char *dir = dirname(executable_path); + size_t dirlen = strlen(dir); + + // sizeof(SERVER_FILENAME) gives statically the size including the null byte + size_t len = dirlen + 1 + sizeof(SERVER_FILENAME); + char *server_path = SDL_malloc(len); + if (!server_path) { + LOGE("Cannot alloc server path string, " + "using " SERVER_FILENAME " from current directory"); + SDL_free(executable_path); + return SERVER_FILENAME; + } + + memcpy(server_path, dir, dirlen); + server_path[dirlen] = PATH_SEPARATOR; + memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME)); + // the final null byte has been copied with SERVER_FILENAME + + SDL_free(executable_path); + + LOGD("Using server (portable): %s", server_path); return server_path; +#endif } static bool @@ -79,27 +115,25 @@ disable_tunnel(struct server *server) { } static process_t -execute_server(const char *serial, - uint16_t max_size, uint32_t bit_rate, - bool tunnel_forward, const char *crop, - bool send_frame_meta) { +execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; char bit_rate_string[11]; - sprintf(max_size_string, "%"PRIu16, max_size); - sprintf(bit_rate_string, "%"PRIu32, bit_rate); + sprintf(max_size_string, "%"PRIu16, params->max_size); + sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); const char *const cmd[] = { "shell", - "CLASSPATH=/data/local/tmp/scrcpy-server.jar", + "CLASSPATH=/data/local/tmp/" SERVER_FILENAME, "app_process", "/", // unused "com.genymobile.scrcpy.Server", max_size_string, bit_rate_string, - tunnel_forward ? "true" : "false", - crop ? crop : "-", - send_frame_meta ? "true" : "false", + server->tunnel_forward ? "true" : "false", + params->crop ? params->crop : "-", + params->send_frame_meta ? "true" : "false", + params->control ? "true" : "false", }; - return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0])); + return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } #define IPV4_LOCALHOST 0x7F000001 @@ -119,7 +153,7 @@ connect_and_read_byte(uint16_t port) { char byte; // the connection may succeed even if the server behind the "adb tunnel" // is not listening, so read one byte to detect a working connection - if (net_recv_all(socket, &byte, 1) != 1) { + if (net_recv(socket, &byte, 1) != 1) { // the server is not listening yet behind the adb tunnel return INVALID_SOCKET; } @@ -160,9 +194,8 @@ server_init(struct server *server) { bool server_start(struct server *server, const char *serial, - uint16_t local_port, uint16_t max_size, uint32_t bit_rate, - const char *crop, bool send_frame_meta) { - server->local_port = local_port; + const struct server_params *params) { + server->local_port = params->local_port; if (serial) { server->serial = SDL_strdup(serial); @@ -191,9 +224,9 @@ server_start(struct server *server, const char *serial, // need to try to connect until the server socket is listening on the // device. - server->server_socket = listen_on_port(local_port); + server->server_socket = listen_on_port(params->local_port); if (server->server_socket == INVALID_SOCKET) { - LOGE("Could not listen on port %" PRIu16, local_port); + LOGE("Could not listen on port %" PRIu16, params->local_port); disable_tunnel(server); SDL_free(server->serial); return false; @@ -201,9 +234,7 @@ server_start(struct server *server, const char *serial, } // server will connect to our server socket - server->process = execute_server(serial, max_size, bit_rate, - server->tunnel_forward, crop, - send_frame_meta); + server->process = execute_server(server, params); if (server->process == PROCESS_NONE) { if (!server->tunnel_forward) { @@ -219,35 +250,58 @@ server_start(struct server *server, const char *serial, return true; } -socket_t +bool server_connect_to(struct server *server) { if (!server->tunnel_forward) { - server->device_socket = net_accept(server->server_socket); + server->video_socket = net_accept(server->server_socket); + if (server->video_socket == INVALID_SOCKET) { + return false; + } + + server->control_socket = net_accept(server->server_socket); + if (server->control_socket == INVALID_SOCKET) { + // the video_socket will be clean up on destroy + return false; + } + + // we don't need the server socket anymore + close_socket(&server->server_socket); } else { uint32_t attempts = 100; uint32_t delay = 100; // ms - server->device_socket = connect_to_server(server->local_port, attempts, - delay); - } + server->video_socket = + connect_to_server(server->local_port, attempts, delay); + if (server->video_socket == INVALID_SOCKET) { + return false; + } - if (server->device_socket == INVALID_SOCKET) { - return INVALID_SOCKET; - } - - if (!server->tunnel_forward) { - // we don't need the server socket anymore - close_socket(&server->server_socket); + // we know that the device is listening, we don't need several attempts + server->control_socket = + net_connect(IPV4_LOCALHOST, server->local_port); + if (server->control_socket == INVALID_SOCKET) { + return false; + } } // we don't need the adb tunnel anymore disable_tunnel(server); // ignore failure server->tunnel_enabled = false; - return server->device_socket; + return true; } void server_stop(struct server *server) { + if (server->server_socket != INVALID_SOCKET) { + close_socket(&server->server_socket); + } + if (server->video_socket != INVALID_SOCKET) { + close_socket(&server->video_socket); + } + if (server->control_socket != INVALID_SOCKET) { + close_socket(&server->control_socket); + } + SDL_assert(server->process != PROCESS_NONE); if (!cmd_terminate(server->process)) { @@ -265,11 +319,5 @@ server_stop(struct server *server) { void server_destroy(struct server *server) { - if (server->server_socket != INVALID_SOCKET) { - close_socket(&server->server_socket); - } - if (server->device_socket != INVALID_SOCKET) { - close_socket(&server->device_socket); - } SDL_free(server->serial); } diff --git a/app/src/server.h b/app/src/server.h index 0f25d48f..74a6cac8 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -11,24 +11,33 @@ struct server { char *serial; process_t process; socket_t server_socket; // only used if !tunnel_forward - socket_t device_socket; + socket_t video_socket; + socket_t control_socket; uint16_t local_port; bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" - bool send_frame_meta; // request frame PTS to be able to record properly }; -#define SERVER_INITIALIZER { \ - .serial = NULL, \ - .process = PROCESS_NONE, \ - .server_socket = INVALID_SOCKET, \ - .device_socket = INVALID_SOCKET, \ - .local_port = 0, \ +#define SERVER_INITIALIZER { \ + .serial = NULL, \ + .process = PROCESS_NONE, \ + .server_socket = INVALID_SOCKET, \ + .video_socket = INVALID_SOCKET, \ + .control_socket = INVALID_SOCKET, \ + .local_port = 0, \ .tunnel_enabled = false, \ .tunnel_forward = false, \ - .send_frame_meta = false, \ } +struct server_params { + const char *crop; + uint16_t local_port; + uint16_t max_size; + uint32_t bit_rate; + bool send_frame_meta; + bool control; +}; + // init default values void server_init(struct server *server); @@ -36,11 +45,10 @@ server_init(struct server *server); // push, enable tunnel et start the server bool server_start(struct server *server, const char *serial, - uint16_t local_port, uint16_t max_size, uint32_t bit_rate, - const char *crop, bool send_frame_meta); + const struct server_params *params); // block until the communication with the server is established -socket_t +bool server_connect_to(struct server *server); // disconnect and kill the server process diff --git a/app/src/str_util.c b/app/src/str_util.c index 3509331a..7d46a1a0 100644 --- a/app/src/str_util.c +++ b/app/src/str_util.c @@ -8,6 +8,8 @@ # include #endif +#include + size_t xstrncpy(char *dest, const char *src, size_t n) { size_t i; @@ -45,7 +47,7 @@ truncated: char * strquote(const char *src) { size_t len = strlen(src); - char *quoted = malloc(len + 3); + char *quoted = SDL_malloc(len + 3); if (!quoted) { return NULL; } @@ -56,6 +58,22 @@ strquote(const char *src) { return quoted; } +size_t +utf8_truncation_index(const char *utf8, size_t max_len) { + size_t len = strlen(utf8); + if (len <= max_len) { + return len; + } + len = max_len; + // see UTF-8 encoding + while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) { + // the next byte is not the start of a new UTF-8 codepoint + // so if we would cut there, the character would be truncated + len--; + } + return len; +} + #ifdef _WIN32 wchar_t * @@ -65,7 +83,7 @@ utf8_to_wide_char(const char *utf8) { return NULL; } - wchar_t *wide = malloc(len * sizeof(wchar_t)); + wchar_t *wide = SDL_malloc(len * sizeof(wchar_t)); if (!wide) { return NULL; } @@ -74,4 +92,20 @@ utf8_to_wide_char(const char *utf8) { return wide; } +char * +utf8_from_wide_char(const wchar_t *ws) { + int len = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); + if (!len) { + return NULL; + } + + char *utf8 = SDL_malloc(len); + if (!utf8) { + return NULL; + } + + WideCharToMultiByte(CP_UTF8, 0, ws, -1, utf8, len, NULL, NULL); + return utf8; +} + #endif diff --git a/app/src/str_util.h b/app/src/str_util.h index 9ef06cbf..0b7a571a 100644 --- a/app/src/str_util.h +++ b/app/src/str_util.h @@ -23,11 +23,18 @@ xstrjoin(char *dst, const char *const tokens[], char sep, size_t n); char * strquote(const char *src); +// return the index to truncate a UTF-8 string at a valid position +size_t +utf8_truncation_index(const char *utf8, size_t max_len); + #ifdef _WIN32 // convert a UTF-8 string to a wchar_t string // returns the new allocated string, to be freed by the caller wchar_t * utf8_to_wide_char(const char *utf8); + +char * +utf8_from_wide_char(const wchar_t *s); #endif #endif diff --git a/app/src/stream.c b/app/src/stream.c index 7ed95ee8..4f38cecf 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -24,7 +24,7 @@ static struct frame_meta * frame_meta_new(uint64_t pts) { - struct frame_meta *meta = malloc(sizeof(*meta)); + struct frame_meta *meta = SDL_malloc(sizeof(*meta)); if (!meta) { return meta; } @@ -35,7 +35,7 @@ frame_meta_new(uint64_t pts) { static void frame_meta_delete(struct frame_meta *frame_meta) { - free(frame_meta); + SDL_free(frame_meta); } static bool @@ -113,7 +113,7 @@ read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { ssize_t r = net_recv(stream->socket, buf, buf_size); if (r == -1) { - return AVERROR(errno); + return errno ? AVERROR(errno) : AVERROR_EOF; } if (r == 0) { return AVERROR_EOF; @@ -130,7 +130,7 @@ read_raw_packet(void *opaque, uint8_t *buf, int buf_size) { struct stream *stream = opaque; ssize_t r = net_recv(stream->socket, buf, buf_size); if (r == -1) { - return AVERROR(errno); + return errno ? AVERROR(errno) : AVERROR_EOF; } if (r == 0) { return AVERROR_EOF; @@ -207,6 +207,13 @@ run_stream(void *data) { packet.size = 0; while (!av_read_frame(format_ctx, &packet)) { + if (SDL_AtomicGet(&stream->stopped)) { + // if the stream is stopped, the socket had been shutdown, so the + // last packet is probably corrupted (but not detected as such by + // FFmpeg) and will not be decoded correctly + av_packet_unref(&packet); + goto quit; + } if (stream->decoder && !decoder_push(stream->decoder, &packet)) { av_packet_unref(&packet); goto quit; @@ -259,6 +266,7 @@ stream_init(struct stream *stream, socket_t socket, stream->socket = socket; stream->decoder = decoder, stream->recorder = recorder; + SDL_AtomicSet(&stream->stopped, 0); } bool @@ -275,6 +283,7 @@ stream_start(struct stream *stream) { void stream_stop(struct stream *stream) { + SDL_AtomicSet(&stream->stopped, 1); if (stream->decoder) { decoder_interrupt(stream->decoder); } diff --git a/app/src/stream.h b/app/src/stream.h index d5eda0ac..1ebff1a0 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -3,6 +3,7 @@ #include #include +#include #include #include "net.h" @@ -18,6 +19,7 @@ struct stream { socket_t socket; struct video_buffer *video_buffer; SDL_Thread *thread; + SDL_atomic_t stopped; struct decoder *decoder; struct recorder *recorder; struct receiver_state { diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index fa41571e..55aea5e8 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -1,9 +1,15 @@ +// for portability #define _POSIX_SOURCE // for kill() +#define _BSD_SOURCE // for readlink() + +// modern glibc will complain without this +#define _DEFAULT_SOURCE #include "command.h" #include #include +#include #include #include #include @@ -98,3 +104,23 @@ cmd_simple_wait(pid_t pid, int *exit_code) { } return !code; } + +char * +get_executable_path(void) { +// +#ifdef __linux__ + char buf[PATH_MAX + 1]; // +1 for the null byte + ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX); + if (len == -1) { + perror("readlink"); + return NULL; + } + buf[len] = '\0'; + return SDL_strdup(buf); +#else + // in practice, we only need this feature for portable builds, only used on + // Windows, so we don't care implementing it for every platform + // (it's useful to have a working version on Linux for debugging though) + return NULL; +#endif +} diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index 1cd7274f..484ce9f0 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -44,7 +44,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) { #endif if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si, &pi)) { - free(wide); + SDL_free(wide); *handle = NULL; if (GetLastError() == ERROR_FILE_NOT_FOUND) { return PROCESS_ERROR_MISSING_BINARY; @@ -52,7 +52,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) { return PROCESS_ERROR_GENERIC; } - free(wide); + SDL_free(wide); *handle = pi.hProcess; return PROCESS_SUCCESS; } @@ -75,3 +75,18 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) { } return !code; } + +char * +get_executable_path(void) { + HMODULE hModule = GetModuleHandleW(NULL); + if (!hModule) { + return NULL; + } + WCHAR buf[MAX_PATH + 1]; // +1 for the null byte + int len = GetModuleFileNameW(hModule, buf, MAX_PATH); + if (!len) { + return NULL; + } + buf[len] = '\0'; + return utf8_from_wide_char(buf); +} diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 8f1d1d9d..2b5f1c2f 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -10,7 +10,10 @@ #include "log.h" bool -video_buffer_init(struct video_buffer *vb) { +video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, + bool render_expired_frames) { + vb->fps_counter = fps_counter; + if (!(vb->decoding_frame = av_frame_alloc())) { goto error_0; } @@ -23,18 +26,20 @@ video_buffer_init(struct video_buffer *vb) { goto error_2; } -#ifndef SKIP_FRAMES - if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) { - SDL_DestroyMutex(vb->mutex); - goto error_2; + vb->render_expired_frames = render_expired_frames; + if (render_expired_frames) { + if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) { + SDL_DestroyMutex(vb->mutex); + goto error_2; + } + // interrupted is not used if expired frames are not rendered + // since offering a frame will never block + vb->interrupted = false; } - vb->interrupted = false; -#endif // there is initially no rendering frame, so consider it has already been // consumed vb->rendering_frame_consumed = true; - fps_counter_init(&vb->fps_counter); return true; @@ -48,9 +53,9 @@ error_0: void video_buffer_destroy(struct video_buffer *vb) { -#ifndef SKIP_FRAMES - SDL_DestroyCond(vb->rendering_frame_consumed_cond); -#endif + if (vb->render_expired_frames) { + SDL_DestroyCond(vb->rendering_frame_consumed_cond); + } SDL_DestroyMutex(vb->mutex); av_frame_free(&vb->rendering_frame); av_frame_free(&vb->decoding_frame); @@ -67,17 +72,14 @@ void video_buffer_offer_decoded_frame(struct video_buffer *vb, bool *previous_frame_skipped) { mutex_lock(vb->mutex); -#ifndef SKIP_FRAMES - // if SKIP_FRAMES is disabled, then the decoder must wait for the current - // frame to be consumed - while (!vb->rendering_frame_consumed && !vb->interrupted) { - cond_wait(vb->rendering_frame_consumed_cond, vb->mutex); + if (vb->render_expired_frames) { + // wait for the current (expired) frame to be consumed + while (!vb->rendering_frame_consumed && !vb->interrupted) { + cond_wait(vb->rendering_frame_consumed_cond, vb->mutex); + } + } else if (!vb->rendering_frame_consumed) { + fps_counter_add_skipped_frame(vb->fps_counter); } -#else - if (vb->fps_counter.started && !vb->rendering_frame_consumed) { - fps_counter_add_skipped_frame(&vb->fps_counter); - } -#endif video_buffer_swap_frames(vb); @@ -91,26 +93,21 @@ const AVFrame * video_buffer_consume_rendered_frame(struct video_buffer *vb) { SDL_assert(!vb->rendering_frame_consumed); vb->rendering_frame_consumed = true; - if (vb->fps_counter.started) { - fps_counter_add_rendered_frame(&vb->fps_counter); + fps_counter_add_rendered_frame(vb->fps_counter); + if (vb->render_expired_frames) { + // unblock video_buffer_offer_decoded_frame() + cond_signal(vb->rendering_frame_consumed_cond); } -#ifndef SKIP_FRAMES - // if SKIP_FRAMES is disabled, then notify the decoder the current frame is - // consumed, so that it may push a new one - cond_signal(vb->rendering_frame_consumed_cond); -#endif return vb->rendering_frame; } void video_buffer_interrupt(struct video_buffer *vb) { -#ifdef SKIP_FRAMES - (void) vb; // unused -#else - mutex_lock(vb->mutex); - vb->interrupted = true; - mutex_unlock(vb->mutex); - // wake up blocking wait - cond_signal(vb->rendering_frame_consumed_cond); -#endif + if (vb->render_expired_frames) { + mutex_lock(vb->mutex); + vb->interrupted = true; + mutex_unlock(vb->mutex); + // wake up blocking wait + cond_signal(vb->rendering_frame_consumed_cond); + } } diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 93222236..26a6fa1f 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -4,7 +4,6 @@ #include #include -#include "config.h" #include "fps_counter.h" // forward declarations @@ -14,16 +13,16 @@ struct video_buffer { AVFrame *decoding_frame; AVFrame *rendering_frame; SDL_mutex *mutex; -#ifndef SKIP_FRAMES + bool render_expired_frames; bool interrupted; SDL_cond *rendering_frame_consumed_cond; -#endif bool rendering_frame_consumed; - struct fps_counter fps_counter; + struct fps_counter *fps_counter; }; bool -video_buffer_init(struct video_buffer *vb); +video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, + bool render_expired_frames); void video_buffer_destroy(struct video_buffer *vb); diff --git a/app/tests/test_cbuf.c b/app/tests/test_cbuf.c new file mode 100644 index 00000000..9d5fdc27 --- /dev/null +++ b/app/tests/test_cbuf.c @@ -0,0 +1,73 @@ +#include +#include + +#include "cbuf.h" + +struct int_queue CBUF(int, 32); + +static void test_cbuf_empty(void) { + struct int_queue queue; + cbuf_init(&queue); + + assert(cbuf_is_empty(&queue)); + + bool push_ok = cbuf_push(&queue, 42); + assert(push_ok); + assert(!cbuf_is_empty(&queue)); + + int item; + bool take_ok = cbuf_take(&queue, &item); + assert(take_ok); + assert(cbuf_is_empty(&queue)); + + bool take_empty_ok = cbuf_take(&queue, &item); + assert(!take_empty_ok); // the queue is empty +} + +static void test_cbuf_full(void) { + struct int_queue queue; + cbuf_init(&queue); + + assert(!cbuf_is_full(&queue)); + + // fill the queue + for (int i = 0; i < 32; ++i) { + bool ok = cbuf_push(&queue, i); + assert(ok); + } + bool ok = cbuf_push(&queue, 42); + assert(!ok); // the queue if full + + int item; + bool take_ok = cbuf_take(&queue, &item); + assert(take_ok); + assert(!cbuf_is_full(&queue)); +} + +static void test_cbuf_push_take(void) { + struct int_queue queue; + cbuf_init(&queue); + + bool push1_ok = cbuf_push(&queue, 42); + assert(push1_ok); + + bool push2_ok = cbuf_push(&queue, 35); + assert(push2_ok); + + int item; + + bool take1_ok = cbuf_take(&queue, &item); + assert(take1_ok); + assert(item == 42); + + bool take2_ok = cbuf_take(&queue, &item); + assert(take2_ok); + assert(item == 35); +} + +int main(void) { + test_cbuf_empty(); + test_cbuf_full(); + test_cbuf_push_take(); + return 0; +} diff --git a/app/tests/test_control_event_queue.c b/app/tests/test_control_event_queue.c deleted file mode 100644 index a27181b6..00000000 --- a/app/tests/test_control_event_queue.c +++ /dev/null @@ -1,95 +0,0 @@ -#include -#include - -#include "control_event.h" - -static void test_control_event_queue_empty(void) { - struct control_event_queue queue; - SDL_bool init_ok = control_event_queue_init(&queue); - assert(init_ok); - - assert(control_event_queue_is_empty(&queue)); - - struct control_event dummy_event; - SDL_bool push_ok = control_event_queue_push(&queue, &dummy_event); - assert(push_ok); - assert(!control_event_queue_is_empty(&queue)); - - SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event); - assert(take_ok); - assert(control_event_queue_is_empty(&queue)); - - SDL_bool take_empty_ok = control_event_queue_take(&queue, &dummy_event); - assert(!take_empty_ok); // the queue is empty - - control_event_queue_destroy(&queue); -} - -static void test_control_event_queue_full(void) { - struct control_event_queue queue; - SDL_bool init_ok = control_event_queue_init(&queue); - assert(init_ok); - - assert(!control_event_queue_is_full(&queue)); - - struct control_event dummy_event; - // fill the queue - while (control_event_queue_push(&queue, &dummy_event)); - - SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event); - assert(take_ok); - assert(!control_event_queue_is_full(&queue)); - - control_event_queue_destroy(&queue); -} - -static void test_control_event_queue_push_take(void) { - struct control_event_queue queue; - SDL_bool init_ok = control_event_queue_init(&queue); - assert(init_ok); - - struct control_event event = { - .type = CONTROL_EVENT_TYPE_KEYCODE, - .keycode_event = { - .action = AKEY_EVENT_ACTION_DOWN, - .keycode = AKEYCODE_ENTER, - .metastate = AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON, - }, - }; - - SDL_bool push1_ok = control_event_queue_push(&queue, &event); - assert(push1_ok); - - event = (struct control_event) { - .type = CONTROL_EVENT_TYPE_TEXT, - .text_event = { - .text = "abc", - }, - }; - - SDL_bool push2_ok = control_event_queue_push(&queue, &event); - assert(push2_ok); - - // overwrite event - SDL_bool take1_ok = control_event_queue_take(&queue, &event); - assert(take1_ok); - assert(event.type == CONTROL_EVENT_TYPE_KEYCODE); - assert(event.keycode_event.action == AKEY_EVENT_ACTION_DOWN); - assert(event.keycode_event.keycode == AKEYCODE_ENTER); - assert(event.keycode_event.metastate == (AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON)); - - // overwrite event - SDL_bool take2_ok = control_event_queue_take(&queue, &event); - assert(take2_ok); - assert(event.type == CONTROL_EVENT_TYPE_TEXT); - assert(!strcmp(event.text_event.text, "abc")); - - control_event_queue_destroy(&queue); -} - -int main(void) { - test_control_event_queue_empty(); - test_control_event_queue_full(); - test_control_event_queue_push_take(); - return 0; -} diff --git a/app/tests/test_control_event_serialize.c b/app/tests/test_control_event_serialize.c deleted file mode 100644 index 90b75fff..00000000 --- a/app/tests/test_control_event_serialize.c +++ /dev/null @@ -1,142 +0,0 @@ -#include -#include - -#include "control_event.h" - -static void test_serialize_keycode_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_KEYCODE, - .keycode_event = { - .action = AKEY_EVENT_ACTION_UP, - .keycode = AKEYCODE_ENTER, - .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, - }, - }; - - unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 10); - - const unsigned char expected[] = { - 0x00, // CONTROL_EVENT_TYPE_KEYCODE - 0x01, // AKEY_EVENT_ACTION_UP - 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER - 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_text_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_TEXT, - .text_event = { - .text = "hello, world!", - }, - }; - - unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 16); - - const unsigned char expected[] = { - 0x01, // CONTROL_EVENT_TYPE_KEYCODE - 0x00, 0x0d, // text length - 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_long_text_event(void) { - struct control_event event; - event.type = CONTROL_EVENT_TYPE_TEXT; - char text[TEXT_MAX_LENGTH + 1]; - memset(text, 'a', sizeof(text)); - text[TEXT_MAX_LENGTH] = '\0'; - event.text_event.text = text; - - unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 3 + TEXT_MAX_LENGTH); - - unsigned char expected[3 + TEXT_MAX_LENGTH]; - expected[0] = 0x01; // CONTROL_EVENT_TYPE_KEYCODE - expected[1] = 0x01; - expected[2] = 0x2c; // text length (16 bits) - memset(&expected[3], 'a', TEXT_MAX_LENGTH); - - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_mouse_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_MOUSE, - .mouse_event = { - .action = AMOTION_EVENT_ACTION_DOWN, - .buttons = AMOTION_EVENT_BUTTON_PRIMARY, - .position = { - .point = { - .x = 260, - .y = 1026, - }, - .screen_size = { - .width = 1080, - .height = 1920, - }, - }, - }, - }; - - unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 18); - - const unsigned char expected[] = { - 0x02, // CONTROL_EVENT_TYPE_MOUSE - 0x00, // AKEY_EVENT_ACTION_DOWN - 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY - 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 - 0x04, 0x38, 0x07, 0x80, // 1080 1920 - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_scroll_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_SCROLL, - .scroll_event = { - .position = { - .point = { - .x = 260, - .y = 1026, - }, - .screen_size = { - .width = 1080, - .height = 1920, - }, - }, - .hscroll = 1, - .vscroll = -1, - }, - }; - - unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 21); - - const unsigned char expected[] = { - 0x03, // CONTROL_EVENT_TYPE_SCROLL - 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 - 0x04, 0x38, 0x07, 0x80, // 1080 1920 - 0x00, 0x00, 0x00, 0x01, // 1 - 0xFF, 0xFF, 0xFF, 0xFF, // -1 - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -int main(void) { - test_serialize_keycode_event(); - test_serialize_text_event(); - test_serialize_long_text_event(); - test_serialize_mouse_event(); - test_serialize_scroll_event(); -} diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c new file mode 100644 index 00000000..c0c501f2 --- /dev/null +++ b/app/tests/test_control_msg_serialize.c @@ -0,0 +1,248 @@ +#include +#include + +#include "control_msg.h" + +static void test_serialize_inject_keycode(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_KEYCODE, + .inject_keycode = { + .action = AKEY_EVENT_ACTION_UP, + .keycode = AKEYCODE_ENTER, + .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 10); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_KEYCODE, + 0x01, // AKEY_EVENT_ACTION_UP + 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER + 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_inject_text(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_TEXT, + .inject_text = { + .text = "hello, world!", + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 16); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_TEXT, + 0x00, 0x0d, // text length + 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_inject_text_long(void) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1]; + memset(text, 'a', sizeof(text)); + text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0'; + msg.inject_text.text = text; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 3 + CONTROL_MSG_TEXT_MAX_LENGTH); + + unsigned char expected[3 + CONTROL_MSG_TEXT_MAX_LENGTH]; + expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; + expected[1] = 0x01; + expected[2] = 0x2c; // text length (16 bits) + memset(&expected[3], 'a', CONTROL_MSG_TEXT_MAX_LENGTH); + + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_inject_mouse_event(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + .inject_mouse_event = { + .action = AMOTION_EVENT_ACTION_DOWN, + .buttons = AMOTION_EVENT_BUTTON_PRIMARY, + .position = { + .point = { + .x = 260, + .y = 1026, + }, + .screen_size = { + .width = 1080, + .height = 1920, + }, + }, + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 18); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + 0x00, // AKEY_EVENT_ACTION_DOWN + 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY + 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 + 0x04, 0x38, 0x07, 0x80, // 1080 1920 + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_inject_scroll_event(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + .inject_scroll_event = { + .position = { + .point = { + .x = 260, + .y = 1026, + }, + .screen_size = { + .width = 1080, + .height = 1920, + }, + }, + .hscroll = 1, + .vscroll = -1, + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 21); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 + 0x04, 0x38, 0x07, 0x80, // 1080 1920 + 0x00, 0x00, 0x00, 0x01, // 1 + 0xFF, 0xFF, 0xFF, 0xFF, // -1 + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_back_or_screen_on(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_expand_notification_panel(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_collapse_notification_panel(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_get_clipboard(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_GET_CLIPBOARD, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_set_clipboard(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, + .inject_text = { + .text = "hello, world!", + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 16); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_SET_CLIPBOARD, + 0x00, 0x0d, // text length + 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_set_screen_power_mode(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + .set_screen_power_mode = { + .mode = SCREEN_POWER_MODE_NORMAL, + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 2); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + 0x02, // SCREEN_POWER_MODE_NORMAL + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +int main(void) { + test_serialize_inject_keycode(); + test_serialize_inject_text(); + test_serialize_inject_text_long(); + test_serialize_inject_mouse_event(); + test_serialize_inject_scroll_event(); + test_serialize_back_or_screen_on(); + test_serialize_expand_notification_panel(); + test_serialize_collapse_notification_panel(); + test_serialize_get_clipboard(); + test_serialize_set_clipboard(); + test_serialize_set_screen_power_mode(); + return 0; +} diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c new file mode 100644 index 00000000..e163ad72 --- /dev/null +++ b/app/tests/test_device_msg_deserialize.c @@ -0,0 +1,28 @@ +#include +#include + +#include "device_msg.h" + +#include +static void test_deserialize_clipboard(void) { + const unsigned char input[] = { + DEVICE_MSG_TYPE_CLIPBOARD, + 0x00, 0x03, // text length + 0x41, 0x42, 0x43, // "ABC" + }; + + struct device_msg msg; + ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); + assert(r == 6); + + assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); + assert(msg.clipboard.text); + assert(!strcmp("ABC", msg.clipboard.text)); + + device_msg_destroy(&msg); +} + +int main(void) { + test_deserialize_clipboard(); + return 0; +} diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 1dd7fbbe..18ac4a7d 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -126,6 +126,37 @@ static void test_xstrjoin_truncated_after_sep(void) { assert(!strcmp("abc de ", s)); } +static void test_utf8_truncate(void) { + const char *s = "aÉbÔc"; + assert(strlen(s) == 7); // É and Ô are 2 bytes-wide + + size_t count; + + count = utf8_truncation_index(s, 1); + assert(count == 1); + + count = utf8_truncation_index(s, 2); + assert(count == 1); // É is 2 bytes-wide + + count = utf8_truncation_index(s, 3); + assert(count == 3); + + count = utf8_truncation_index(s, 4); + assert(count == 4); + + count = utf8_truncation_index(s, 5); + assert(count == 4); // Ô is 2 bytes-wide + + count = utf8_truncation_index(s, 6); + assert(count == 6); + + count = utf8_truncation_index(s, 7); + assert(count == 7); + + count = utf8_truncation_index(s, 8); + assert(count == 7); // no more chars +} + int main(void) { test_xstrncpy_simple(); test_xstrncpy_just_fit(); @@ -135,5 +166,6 @@ int main(void) { test_xstrjoin_truncated_in_token(); test_xstrjoin_truncated_before_sep(); test_xstrjoin_truncated_after_sep(); + test_utf8_truncate(); return 0; } diff --git a/build.gradle b/build.gradle index a96495fc..1b6f5aef 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.1' + classpath 'com.android.tools.build:gradle:3.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/cross_win32.txt b/cross_win32.txt index f82056ff..2db35fe0 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1-win32-dev' -prebuilt_sdl2 = 'SDL2-2.0.9/i686-w64-mingw32' +prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win32-dev' +prebuilt_sdl2 = 'SDL2-2.0.8/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index beca2096..79181653 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1-win64-dev' -prebuilt_sdl2 = 'SDL2-2.0.9/x86_64-w64-mingw32' +prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win64-dev' +prebuilt_sdl2 = 'SDL2-2.0.8/x86_64-w64-mingw32' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8ba12c0c..33997651 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jun 04 11:48:32 CEST 2018 +#Thu Apr 18 11:45:59 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip diff --git a/meson.build b/meson.build index eb7ee380..053d8c94 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.8', + version: '1.9', meson_version: '>= 0.37', default_options: 'c_std=c11') diff --git a/meson_options.txt b/meson_options.txt index 567f0a39..a443ccb2 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,6 +3,6 @@ option('build_server', type: 'boolean', value: true, description: 'Build the ser option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') -option('override_server_path', type: 'string', description: 'Hardcoded path to find the server at runtime') +option('portable', type: 'boolean', description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') option('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index f3c171ba..04f8b779 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -10,31 +10,31 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-ffmpeg-shared-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1-win32-shared.zip \ - e692b18c01745d262c03294b382fd64df68fabe3c66aa4546a3ad3935175cde3 \ - ffmpeg-4.1-win32-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.3-win32-shared.zip \ + 8ea472d673370d5e87517a75587abfa6f189ee4f82e8da21fdbc49d0db0c1a89 \ + ffmpeg-4.1.3-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1-win32-dev.zip \ - 34bc5e471fb9160609abd6bc271e361050f3ff7376b1b8a0873cca02b38277c8 \ - ffmpeg-4.1-win32-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.3-win32-dev.zip \ + e16d3150b6ccf0b71908f5b964cb8c051d79053c8f5cd6d777d617ab4f03613a \ + ffmpeg-4.1.3-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1-win64-shared.zip \ - c4908c97436c946509dc365e421159274fa4b1e66dce6fb5b63d82a6294d5357 \ - ffmpeg-4.1-win64-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.3-win64-shared.zip \ + 0b974578e07d974c4bafb36c7ed0b46e46b001d38b149455089c13b57ddefe5d \ + ffmpeg-4.1.3-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1-win64-dev.zip \ - 761ec79aa3dae66698c9791a2f0bb9da8794246f8356cadc741ddc0eabab0471 \ - ffmpeg-4.1-win64-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.3-win64-dev.zip \ + 334b473467db096a5b74242743592a73e120a137232794508e4fc55593696a5b \ + ffmpeg-4.1.3-win64-dev prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.9-mingw.tar.gz \ - 0f9f00d0f2a9a95dfb5cce929718210c3f85432cc2e9d4abade4adcb7f6bb39d \ - SDL2-2.0.9 + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.8-mingw.tar.gz \ + ffff7305d634aff5e1df5b7bb935435c3a02c8b03ad94a1a2be9169a558a7961 \ + SDL2-2.0.8 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r28.0.1-windows.zip \ - db78f726d5dc653706dcd15a462ab1b946c643f598df76906c4c1858411c54df \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.1-windows.zip \ + 2334f92cf571fd2d9bf6ff7c637765bee5d8323e0bd8e051e15927d87b54b4e8 \ platform-tools diff --git a/server/build.gradle b/server/build.gradle index 65d1e558..d5c1fb00 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 27 + compileSdkVersion 29 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 27 - versionCode 9 - versionName "1.8" + targetSdkVersion 29 + versionCode 10 + versionName "1.9" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/meson.build b/server/meson.build index 327e6aca..d96373a0 100644 --- a/server/meson.build +++ b/server/meson.build @@ -4,9 +4,8 @@ prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', build_always: true, # gradle is responsible for tracking source changes - input: '.', output: 'scrcpy-server.jar', - command: [find_program('./scripts/build-wrapper.sh'), '@INPUT@', '@OUTPUT@', get_option('buildtype')], + command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], install: true, install_dir: 'share/scrcpy') else diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java b/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java deleted file mode 100644 index 8b0f9e2b..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.genymobile.scrcpy; - -/** - * Union of all supported event types, identified by their {@code type}. - */ -public final class ControlEvent { - - public static final int TYPE_KEYCODE = 0; - public static final int TYPE_TEXT = 1; - public static final int TYPE_MOUSE = 2; - public static final int TYPE_SCROLL = 3; - public static final int TYPE_COMMAND = 4; - - public static final int COMMAND_BACK_OR_SCREEN_ON = 0; - public static final int COMMAND_EXPAND_NOTIFICATION_PANEL = 1; - public static final int COMMAND_COLLAPSE_NOTIFICATION_PANEL = 2; - - private int type; - private String text; - private int metaState; // KeyEvent.META_* - private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or COMMAND_* - private int keycode; // KeyEvent.KEYCODE_* - private int buttons; // MotionEvent.BUTTON_* - private Position position; - private int hScroll; - private int vScroll; - - private ControlEvent() { - } - - public static ControlEvent createKeycodeControlEvent(int action, int keycode, int metaState) { - ControlEvent event = new ControlEvent(); - event.type = TYPE_KEYCODE; - event.action = action; - event.keycode = keycode; - event.metaState = metaState; - return event; - } - - public static ControlEvent createTextControlEvent(String text) { - ControlEvent event = new ControlEvent(); - event.type = TYPE_TEXT; - event.text = text; - return event; - } - - public static ControlEvent createMotionControlEvent(int action, int buttons, Position position) { - ControlEvent event = new ControlEvent(); - event.type = TYPE_MOUSE; - event.action = action; - event.buttons = buttons; - event.position = position; - return event; - } - - public static ControlEvent createScrollControlEvent(Position position, int hScroll, int vScroll) { - ControlEvent event = new ControlEvent(); - event.type = TYPE_SCROLL; - event.position = position; - event.hScroll = hScroll; - event.vScroll = vScroll; - return event; - } - - public static ControlEvent createCommandControlEvent(int action) { - ControlEvent event = new ControlEvent(); - event.type = TYPE_COMMAND; - event.action = action; - return event; - } - - public int getType() { - return type; - } - - public String getText() { - return text; - } - - public int getMetaState() { - return metaState; - } - - public int getAction() { - return action; - } - - public int getKeycode() { - return keycode; - } - - public int getButtons() { - return buttons; - } - - public Position getPosition() { - return position; - } - - public int getHScroll() { - return hScroll; - } - - public int getVScroll() { - return vScroll; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java deleted file mode 100644 index 28e9503a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.genymobile.scrcpy; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; - -public class ControlEventReader { - - private static final int KEYCODE_PAYLOAD_LENGTH = 9; - private static final int MOUSE_PAYLOAD_LENGTH = 17; - private static final int SCROLL_PAYLOAD_LENGTH = 20; - private static final int COMMAND_PAYLOAD_LENGTH = 1; - - public static final int TEXT_MAX_LENGTH = 300; - private static final int RAW_BUFFER_SIZE = 1024; - - private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; - private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); - private final byte[] textBuffer = new byte[TEXT_MAX_LENGTH]; - - public ControlEventReader() { - // invariant: the buffer is always in "get" mode - buffer.limit(0); - } - - public boolean isFull() { - return buffer.remaining() == rawBuffer.length; - } - - public void readFrom(InputStream input) throws IOException { - if (isFull()) { - throw new IllegalStateException("Buffer full, call next() to consume"); - } - buffer.compact(); - int head = buffer.position(); - int r = input.read(rawBuffer, head, rawBuffer.length - head); - if (r == -1) { - throw new EOFException("Event controller socket closed"); - } - buffer.position(head + r); - buffer.flip(); - } - - public ControlEvent next() { - if (!buffer.hasRemaining()) { - return null; - } - int savedPosition = buffer.position(); - - int type = buffer.get(); - ControlEvent controlEvent; - switch (type) { - case ControlEvent.TYPE_KEYCODE: - controlEvent = parseKeycodeControlEvent(); - break; - case ControlEvent.TYPE_TEXT: - controlEvent = parseTextControlEvent(); - break; - case ControlEvent.TYPE_MOUSE: - controlEvent = parseMouseControlEvent(); - break; - case ControlEvent.TYPE_SCROLL: - controlEvent = parseScrollControlEvent(); - break; - case ControlEvent.TYPE_COMMAND: - controlEvent = parseCommandControlEvent(); - break; - default: - Ln.w("Unknown event type: " + type); - controlEvent = null; - break; - } - - if (controlEvent == null) { - // failure, reset savedPosition - buffer.position(savedPosition); - } - return controlEvent; - } - - private ControlEvent parseKeycodeControlEvent() { - if (buffer.remaining() < KEYCODE_PAYLOAD_LENGTH) { - return null; - } - int action = toUnsigned(buffer.get()); - int keycode = buffer.getInt(); - int metaState = buffer.getInt(); - return ControlEvent.createKeycodeControlEvent(action, keycode, metaState); - } - - private ControlEvent parseTextControlEvent() { - if (buffer.remaining() < 1) { - return null; - } - int len = toUnsigned(buffer.getShort()); - if (buffer.remaining() < len) { - return null; - } - buffer.get(textBuffer, 0, len); - String text = new String(textBuffer, 0, len, StandardCharsets.UTF_8); - return ControlEvent.createTextControlEvent(text); - } - - private ControlEvent parseMouseControlEvent() { - if (buffer.remaining() < MOUSE_PAYLOAD_LENGTH) { - return null; - } - int action = toUnsigned(buffer.get()); - int buttons = buffer.getInt(); - Position position = readPosition(buffer); - return ControlEvent.createMotionControlEvent(action, buttons, position); - } - - private ControlEvent parseScrollControlEvent() { - if (buffer.remaining() < SCROLL_PAYLOAD_LENGTH) { - return null; - } - Position position = readPosition(buffer); - int hScroll = buffer.getInt(); - int vScroll = buffer.getInt(); - return ControlEvent.createScrollControlEvent(position, hScroll, vScroll); - } - - private ControlEvent parseCommandControlEvent() { - if (buffer.remaining() < COMMAND_PAYLOAD_LENGTH) { - return null; - } - int action = toUnsigned(buffer.get()); - return ControlEvent.createCommandControlEvent(action); - } - - private static Position readPosition(ByteBuffer buffer) { - int x = buffer.getInt(); - int y = buffer.getInt(); - int screenWidth = toUnsigned(buffer.getShort()); - int screenHeight = toUnsigned(buffer.getShort()); - return new Position(x, y, screenWidth, screenHeight); - } - - @SuppressWarnings("checkstyle:MagicNumber") - private static int toUnsigned(short value) { - return value & 0xffff; - } - - @SuppressWarnings("checkstyle:MagicNumber") - private static int toUnsigned(byte value) { - return value & 0xff; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java new file mode 100644 index 00000000..0de4bc3c --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -0,0 +1,124 @@ +package com.genymobile.scrcpy; + +/** + * Union of all supported event types, identified by their {@code type}. + */ +public final class ControlMessage { + + public static final int TYPE_INJECT_KEYCODE = 0; + public static final int TYPE_INJECT_TEXT = 1; + public static final int TYPE_INJECT_MOUSE_EVENT = 2; + public static final int TYPE_INJECT_SCROLL_EVENT = 3; + public static final int TYPE_BACK_OR_SCREEN_ON = 4; + public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; + public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; + public static final int TYPE_GET_CLIPBOARD = 7; + public static final int TYPE_SET_CLIPBOARD = 8; + public static final int TYPE_SET_SCREEN_POWER_MODE = 9; + + private int type; + private String text; + private int metaState; // KeyEvent.META_* + private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* + private int keycode; // KeyEvent.KEYCODE_* + private int buttons; // MotionEvent.BUTTON_* + private Position position; + private int hScroll; + private int vScroll; + + private ControlMessage() { + } + + public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_KEYCODE; + event.action = action; + event.keycode = keycode; + event.metaState = metaState; + return event; + } + + public static ControlMessage createInjectText(String text) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_TEXT; + event.text = text; + return event; + } + + public static ControlMessage createInjectMouseEvent(int action, int buttons, Position position) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_MOUSE_EVENT; + event.action = action; + event.buttons = buttons; + event.position = position; + return event; + } + + public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_SCROLL_EVENT; + event.position = position; + event.hScroll = hScroll; + event.vScroll = vScroll; + return event; + } + + public static ControlMessage createSetClipboard(String text) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_SET_CLIPBOARD; + event.text = text; + return event; + } + + /** + * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants + */ + public static ControlMessage createSetScreenPowerMode(int mode) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_SET_SCREEN_POWER_MODE; + event.action = mode; + return event; + } + + public static ControlMessage createEmpty(int type) { + ControlMessage event = new ControlMessage(); + event.type = type; + return event; + } + + public int getType() { + return type; + } + + public String getText() { + return text; + } + + public int getMetaState() { + return metaState; + } + + public int getAction() { + return action; + } + + public int getKeycode() { + return keycode; + } + + public int getButtons() { + return buttons; + } + + public Position getPosition() { + return position; + } + + public int getHScroll() { + return hScroll; + } + + public int getVScroll() { + return vScroll; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java new file mode 100644 index 00000000..8ced049d --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -0,0 +1,176 @@ +package com.genymobile.scrcpy; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class ControlMessageReader { + + private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; + private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; + private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; + private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + + public static final int TEXT_MAX_LENGTH = 300; + public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; + private static final int RAW_BUFFER_SIZE = 1024; + + private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; + private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH]; + + public ControlMessageReader() { + // invariant: the buffer is always in "get" mode + buffer.limit(0); + } + + public boolean isFull() { + return buffer.remaining() == rawBuffer.length; + } + + public void readFrom(InputStream input) throws IOException { + if (isFull()) { + throw new IllegalStateException("Buffer full, call next() to consume"); + } + buffer.compact(); + int head = buffer.position(); + int r = input.read(rawBuffer, head, rawBuffer.length - head); + if (r == -1) { + throw new EOFException("Controller socket closed"); + } + buffer.position(head + r); + buffer.flip(); + } + + public ControlMessage next() { + if (!buffer.hasRemaining()) { + return null; + } + int savedPosition = buffer.position(); + + int type = buffer.get(); + ControlMessage msg; + switch (type) { + case ControlMessage.TYPE_INJECT_KEYCODE: + msg = parseInjectKeycode(); + break; + case ControlMessage.TYPE_INJECT_TEXT: + msg = parseInjectText(); + break; + case ControlMessage.TYPE_INJECT_MOUSE_EVENT: + msg = parseInjectMouseEvent(); + break; + case ControlMessage.TYPE_INJECT_SCROLL_EVENT: + msg = parseInjectScrollEvent(); + break; + case ControlMessage.TYPE_SET_CLIPBOARD: + msg = parseSetClipboard(); + break; + case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + msg = parseSetScreenPowerMode(); + break; + case ControlMessage.TYPE_BACK_OR_SCREEN_ON: + case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: + case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: + case ControlMessage.TYPE_GET_CLIPBOARD: + msg = ControlMessage.createEmpty(type); + break; + default: + Ln.w("Unknown event type: " + type); + msg = null; + break; + } + + if (msg == null) { + // failure, reset savedPosition + buffer.position(savedPosition); + } + return msg; + } + + private ControlMessage parseInjectKeycode() { + if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) { + return null; + } + int action = toUnsigned(buffer.get()); + int keycode = buffer.getInt(); + int metaState = buffer.getInt(); + return ControlMessage.createInjectKeycode(action, keycode, metaState); + } + + private String parseString() { + if (buffer.remaining() < 2) { + return null; + } + int len = toUnsigned(buffer.getShort()); + if (buffer.remaining() < len) { + return null; + } + buffer.get(textBuffer, 0, len); + return new String(textBuffer, 0, len, StandardCharsets.UTF_8); + } + + private ControlMessage parseInjectText() { + String text = parseString(); + if (text == null) { + return null; + } + return ControlMessage.createInjectText(text); + } + + private ControlMessage parseInjectMouseEvent() { + if (buffer.remaining() < INJECT_MOUSE_EVENT_PAYLOAD_LENGTH) { + return null; + } + int action = toUnsigned(buffer.get()); + int buttons = buffer.getInt(); + Position position = readPosition(buffer); + return ControlMessage.createInjectMouseEvent(action, buttons, position); + } + + private ControlMessage parseInjectScrollEvent() { + if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) { + return null; + } + Position position = readPosition(buffer); + int hScroll = buffer.getInt(); + int vScroll = buffer.getInt(); + return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); + } + + private ControlMessage parseSetClipboard() { + String text = parseString(); + if (text == null) { + return null; + } + return ControlMessage.createSetClipboard(text); + } + + private ControlMessage parseSetScreenPowerMode() { + if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) { + return null; + } + int mode = buffer.get(); + return ControlMessage.createSetScreenPowerMode(mode); + } + + private static Position readPosition(ByteBuffer buffer) { + int x = buffer.getInt(); + int y = buffer.getInt(); + int screenWidth = toUnsigned(buffer.getShort()); + int screenHeight = toUnsigned(buffer.getShort()); + return new Position(x, y, screenWidth, screenHeight); + } + + @SuppressWarnings("checkstyle:MagicNumber") + private static int toUnsigned(short value) { + return value & 0xffff; + } + + @SuppressWarnings("checkstyle:MagicNumber") + private static int toUnsigned(byte value) { + return value & 0xff; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java similarity index 64% rename from server/src/main/java/com/genymobile/scrcpy/EventController.java rename to server/src/main/java/com/genymobile/scrcpy/Controller.java index 341869fa..263fc2fc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -2,7 +2,6 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.InputManager; -import android.graphics.Point; import android.os.SystemClock; import android.view.InputDevice; import android.view.InputEvent; @@ -12,11 +11,11 @@ import android.view.MotionEvent; import java.io.IOException; - -public class EventController { +public class Controller { private final Device device; private final DesktopConnection connection; + private final DeviceMessageSender sender; private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); @@ -24,10 +23,11 @@ public class EventController { private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()}; private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()}; - public EventController(Device device, DesktopConnection connection) { + public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; initPointer(); + sender = new DeviceMessageSender(connection); } private void initPointer() { @@ -43,8 +43,8 @@ public class EventController { private void setPointerCoords(Point point) { MotionEvent.PointerCoords coords = pointerCoords[0]; - coords.x = point.x; - coords.y = point.y; + coords.x = point.getX(); + coords.y = point.getY(); } private void setScroll(int hScroll, int vScroll) { @@ -53,32 +53,64 @@ public class EventController { coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); } + @SuppressWarnings("checkstyle:MagicNumber") public void control() throws IOException { - // on start, turn screen on - turnScreenOn(); + // on start, power on the device + if (!device.isScreenOn()) { + injectKeycode(KeyEvent.KEYCODE_POWER); + + // dirty hack + // After POWER is injected, the device is powered on asynchronously. + // To turn the device screen off while mirroring, the client will send a message that + // would be handled before the device is actually powered on, so its effect would + // be "canceled" once the device is turned back on. + // Adding this delay prevents to handle the message before the device is actually + // powered on. + SystemClock.sleep(500); + } while (true) { handleEvent(); } } + public DeviceMessageSender getSender() { + return sender; + } + private void handleEvent() throws IOException { - ControlEvent controlEvent = connection.receiveControlEvent(); - switch (controlEvent.getType()) { - case ControlEvent.TYPE_KEYCODE: - injectKeycode(controlEvent.getAction(), controlEvent.getKeycode(), controlEvent.getMetaState()); + ControlMessage msg = connection.receiveControlMessage(); + switch (msg.getType()) { + case ControlMessage.TYPE_INJECT_KEYCODE: + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); break; - case ControlEvent.TYPE_TEXT: - injectText(controlEvent.getText()); + case ControlMessage.TYPE_INJECT_TEXT: + injectText(msg.getText()); break; - case ControlEvent.TYPE_MOUSE: - injectMouse(controlEvent.getAction(), controlEvent.getButtons(), controlEvent.getPosition()); + case ControlMessage.TYPE_INJECT_MOUSE_EVENT: + injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition()); break; - case ControlEvent.TYPE_SCROLL: - injectScroll(controlEvent.getPosition(), controlEvent.getHScroll(), controlEvent.getVScroll()); + case ControlMessage.TYPE_INJECT_SCROLL_EVENT: + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); break; - case ControlEvent.TYPE_COMMAND: - executeCommand(controlEvent.getAction()); + case ControlMessage.TYPE_BACK_OR_SCREEN_ON: + pressBackOrTurnScreenOn(); + break; + case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: + device.expandNotificationPanel(); + break; + case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: + device.collapsePanels(); + break; + case ControlMessage.TYPE_GET_CLIPBOARD: + String clipboardText = device.getClipboardText(); + sender.pushClipboardText(clipboardText); + break; + case ControlMessage.TYPE_SET_CLIPBOARD: + device.setClipboardText(msg.getText()); + break; + case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + device.setScreenPowerMode(msg.getAction()); break; default: // do nothing @@ -91,7 +123,7 @@ public class EventController { private boolean injectChar(char c) { String decomposed = KeyComposition.decompose(c); - char[] chars = decomposed != null ? decomposed.toCharArray() : new char[] {c}; + char[] chars = decomposed != null ? decomposed.toCharArray() : new char[]{c}; KeyEvent[] events = charMap.getEvents(chars); if (events == null) { return false; @@ -104,13 +136,16 @@ public class EventController { return true; } - private boolean injectText(String text) { + private int injectText(String text) { + int successCount = 0; for (char c : text.toCharArray()) { if (!injectChar(c)) { - return false; + Ln.w("Could not inject char u+" + String.format("%04x", (int) c)); + continue; } + successCount++; } - return true; + return successCount; } private boolean injectMouse(int action, int buttons, Position position) { @@ -159,28 +194,8 @@ public class EventController { return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); } - private boolean turnScreenOn() { - return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER); - } - private boolean pressBackOrTurnScreenOn() { int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; return injectKeycode(keycode); } - - private boolean executeCommand(int action) { - switch (action) { - case ControlEvent.COMMAND_BACK_OR_SCREEN_ON: - return pressBackOrTurnScreenOn(); - case ControlEvent.COMMAND_EXPAND_NOTIFICATION_PANEL: - device.expandNotificationPanel(); - return true; - case ControlEvent.COMMAND_COLLAPSE_NOTIFICATION_PANEL: - device.collapsePanels(); - return true; - default: - Ln.w("Unsupported command: " + action); - } - return false; - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index d87a7fd8..a725d83d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -8,6 +8,7 @@ import java.io.Closeable; import java.io.FileDescriptor; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; public final class DesktopConnection implements Closeable { @@ -16,16 +17,22 @@ public final class DesktopConnection implements Closeable { private static final String SOCKET_NAME = "scrcpy"; - private final LocalSocket socket; - private final InputStream inputStream; - private final FileDescriptor fd; + private final LocalSocket videoSocket; + private final FileDescriptor videoFd; - private final ControlEventReader reader = new ControlEventReader(); + private final LocalSocket controlSocket; + private final InputStream controlInputStream; + private final OutputStream controlOutputStream; - private DesktopConnection(LocalSocket socket) throws IOException { - this.socket = socket; - inputStream = socket.getInputStream(); - fd = socket.getFileDescriptor(); + private final ControlMessageReader reader = new ControlMessageReader(); + private final DeviceMessageWriter writer = new DeviceMessageWriter(); + + private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException { + this.videoSocket = videoSocket; + this.controlSocket = controlSocket; + controlInputStream = controlSocket.getInputStream(); + controlOutputStream = controlSocket.getOutputStream(); + videoFd = videoSocket.getFileDescriptor(); } private static LocalSocket connect(String abstractName) throws IOException { @@ -34,35 +41,47 @@ public final class DesktopConnection implements Closeable { return localSocket; } - private static LocalSocket listenAndAccept(String abstractName) throws IOException { - LocalServerSocket localServerSocket = new LocalServerSocket(abstractName); - try { - return localServerSocket.accept(); - } finally { - localServerSocket.close(); - } - } - public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException { - LocalSocket socket; + LocalSocket videoSocket; + LocalSocket controlSocket; if (tunnelForward) { - socket = listenAndAccept(SOCKET_NAME); - // send one byte so the client may read() to detect a connection error - socket.getOutputStream().write(0); + LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME); + try { + videoSocket = localServerSocket.accept(); + // send one byte so the client may read() to detect a connection error + videoSocket.getOutputStream().write(0); + try { + controlSocket = localServerSocket.accept(); + } catch (IOException | RuntimeException e) { + videoSocket.close(); + throw e; + } + } finally { + localServerSocket.close(); + } } else { - socket = connect(SOCKET_NAME); + videoSocket = connect(SOCKET_NAME); + try { + controlSocket = connect(SOCKET_NAME); + } catch (IOException | RuntimeException e) { + videoSocket.close(); + throw e; + } } - DesktopConnection connection = new DesktopConnection(socket); + DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket); Size videoSize = device.getScreenInfo().getVideoSize(); connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); return connection; } public void close() throws IOException { - socket.shutdownInput(); - socket.shutdownOutput(); - socket.close(); + videoSocket.shutdownInput(); + videoSocket.shutdownOutput(); + videoSocket.close(); + controlSocket.shutdownInput(); + controlSocket.shutdownOutput(); + controlSocket.close(); } @SuppressWarnings("checkstyle:MagicNumber") @@ -70,7 +89,7 @@ public final class DesktopConnection implements Closeable { byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8); - int len = Math.min(DEVICE_NAME_FIELD_LENGTH - 1, deviceNameBytes.length); + int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1); System.arraycopy(deviceNameBytes, 0, buffer, 0, len); // byte[] are always 0-initialized in java, no need to set '\0' explicitly @@ -78,19 +97,23 @@ public final class DesktopConnection implements Closeable { buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width; buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8); buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height; - IO.writeFully(fd, buffer, 0, buffer.length); + IO.writeFully(videoFd, buffer, 0, buffer.length); } - public FileDescriptor getFd() { - return fd; + public FileDescriptor getVideoFd() { + return videoFd; } - public ControlEvent receiveControlEvent() throws IOException { - ControlEvent event = reader.next(); - while (event == null) { - reader.readFrom(inputStream); - event = reader.next(); + public ControlMessage receiveControlMessage() throws IOException { + ControlMessage msg = reader.next(); + while (msg == null) { + reader.readFrom(controlInputStream); + msg = reader.next(); } - return event; + return msg; + } + + public void sendDeviceMessage(DeviceMessage msg) throws IOException { + writer.writeTo(msg, controlOutputStream); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 60122c5a..538135d4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,16 +1,20 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; +import com.genymobile.scrcpy.wrappers.SurfaceControl; -import android.graphics.Point; import android.graphics.Rect; import android.os.Build; +import android.os.IBinder; import android.os.RemoteException; import android.view.IRotationWatcher; import android.view.InputEvent; public final class Device { + public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; + public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; + public interface RotationListener { void onRotationChanged(int rotation); } @@ -107,8 +111,8 @@ public final class Device { } Rect contentRect = screenInfo.getContentRect(); Point point = position.getPoint(); - int scaledX = contentRect.left + point.x * contentRect.width() / videoSize.getWidth(); - int scaledY = contentRect.top + point.y * contentRect.height() / videoSize.getHeight(); + int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); + int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); return new Point(scaledX, scaledY); } @@ -140,6 +144,28 @@ public final class Device { serviceManager.getStatusBarManager().collapsePanels(); } + public String getClipboardText() { + CharSequence s = serviceManager.getClipboardManager().getText(); + if (s == null) { + return null; + } + return s.toString(); + } + + public void setClipboardText(String text) { + serviceManager.getClipboardManager().setText(text); + Ln.i("Device clipboard set"); + } + + /** + * @param mode one of the {@code SCREEN_POWER_MODE_*} constants + */ + public void setScreenPowerMode(int mode) { + IBinder d = SurfaceControl.getBuiltInDisplay(0); + SurfaceControl.setDisplayPowerMode(d, mode); + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + } + static Rect flipRect(Rect crop) { return new Rect(crop.top, crop.left, crop.bottom, crop.right); } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java new file mode 100644 index 00000000..c6eebd38 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java @@ -0,0 +1,27 @@ +package com.genymobile.scrcpy; + +public final class DeviceMessage { + + public static final int TYPE_CLIPBOARD = 0; + + private int type; + private String text; + + private DeviceMessage() { + } + + public static DeviceMessage createClipboard(String text) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_CLIPBOARD; + event.text = text; + return event; + } + + public int getType() { + return type; + } + + public String getText() { + return text; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java new file mode 100644 index 00000000..bbf4dd2e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -0,0 +1,34 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; + +public final class DeviceMessageSender { + + private final DesktopConnection connection; + + private String clipboardText; + + public DeviceMessageSender(DesktopConnection connection) { + this.connection = connection; + } + + public synchronized void pushClipboardText(String text) { + clipboardText = text; + notify(); + } + + public void loop() throws IOException, InterruptedException { + while (true) { + String text; + synchronized (this) { + while (clipboardText == null) { + wait(); + } + text = clipboardText; + clipboardText = null; + } + DeviceMessage event = DeviceMessage.createClipboard(text); + connection.sendDeviceMessage(event); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java new file mode 100644 index 00000000..e2a3a1a2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -0,0 +1,34 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class DeviceMessageWriter { + + public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; + private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3; + + private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; + private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + + @SuppressWarnings("checkstyle:MagicNumber") + public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { + buffer.clear(); + buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD); + switch (msg.getType()) { + case DeviceMessage.TYPE_CLIPBOARD: + String text = msg.getText(); + byte[] raw = text.getBytes(StandardCharsets.UTF_8); + int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); + buffer.putShort((short) len); + buffer.put(raw, 0, len); + output.write(rawBuffer, 0, buffer.position()); + break; + default: + Ln.w("Unknown device message: " + msg.getType()); + break; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index 9364519e..bb741225 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -9,6 +9,7 @@ import android.util.Log; public final class Ln { private static final String TAG = "scrcpy"; + private static final String PREFIX = "[server] "; enum Level { DEBUG, @@ -30,29 +31,35 @@ public final class Ln { public static void d(String message) { if (isEnabled(Level.DEBUG)) { Log.d(TAG, message); - System.out.println("DEBUG: " + message); + System.out.println(PREFIX + "DEBUG: " + message); } } public static void i(String message) { if (isEnabled(Level.INFO)) { Log.i(TAG, message); - System.out.println("INFO: " + message); + System.out.println(PREFIX + "INFO: " + message); } } public static void w(String message) { if (isEnabled(Level.WARN)) { Log.w(TAG, message); - System.out.println("WARN: " + message); + System.out.println(PREFIX + "WARN: " + message); } } public static void e(String message, Throwable throwable) { if (isEnabled(Level.ERROR)) { Log.e(TAG, message, throwable); - System.out.println("ERROR: " + message); - throwable.printStackTrace(); + System.out.println(PREFIX + "ERROR: " + message); + if (throwable != null) { + throwable.printStackTrace(); + } } } + + public static void e(String message) { + e(message, null); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 851c7ed6..af6b2ee1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -8,6 +8,7 @@ public class Options { private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly + private boolean control; public int getMaxSize() { return maxSize; @@ -48,4 +49,12 @@ public class Options { public void setSendFrameMeta(boolean sendFrameMeta) { this.sendFrameMeta = sendFrameMeta; } + + public boolean getControl() { + return control; + } + + public void setControl(boolean control) { + this.control = control; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Point.java b/server/src/main/java/com/genymobile/scrcpy/Point.java new file mode 100644 index 00000000..9ef2db03 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Point.java @@ -0,0 +1,47 @@ +package com.genymobile.scrcpy; + +import java.util.Objects; + +public class Point { + private final int x; + private final int y; + + public Point(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Point point = (Point) o; + return x == point.x + && y == point.y; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); + } + + @Override + public String toString() { + return "Point{" + + "x=" + x + + ", y=" + y + + '}'; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index e00a6355..757fa36e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -1,7 +1,5 @@ package com.genymobile.scrcpy; -import android.graphics.Point; - import java.util.Objects; public class Position { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index c946e990..8357b061 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -70,8 +70,9 @@ public class ScreenEncoder implements Device.RotationListener { codec.start(); try { alive = encode(codec, fd); - } finally { + // do not call stop() on exception, it would trigger an IllegalStateException codec.stop(); + } finally { destroyDisplay(display); codec.release(); surface.release(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index b782101c..1e4d10d6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -19,12 +19,17 @@ public final class Server { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate()); - // asynchronous - startEventController(device, connection); + if (options.getControl()) { + Controller controller = new Controller(device, connection); + + // asynchronous + startController(controller); + startDeviceMessageSender(controller.getSender()); + } try { // synchronous - screenEncoder.streamScreen(device, connection.getFd()); + screenEncoder.streamScreen(device, connection.getVideoFd()); } catch (IOException e) { // this is expected on close Ln.d("Screen streaming stopped"); @@ -32,15 +37,29 @@ public final class Server { } } - private static void startEventController(final Device device, final DesktopConnection connection) { + private static void startController(final Controller controller) { new Thread(new Runnable() { @Override public void run() { try { - new EventController(device, connection).control(); + controller.control(); } catch (IOException e) { // this is expected on close - Ln.d("Event controller stopped"); + Ln.d("Controller stopped"); + } + } + }).start(); + } + + private static void startDeviceMessageSender(final DeviceMessageSender sender) { + new Thread(new Runnable() { + @Override + public void run() { + try { + sender.loop(); + } catch (IOException | InterruptedException e) { + // this is expected on close + Ln.d("Device message sender stopped"); } } }).start(); @@ -48,7 +67,7 @@ public final class Server { @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { - if (args.length != 5) { + if (args.length != 6) { throw new IllegalArgumentException("Expecting 5 parameters"); } @@ -70,6 +89,9 @@ public final class Server { boolean sendFrameMeta = Boolean.parseBoolean(args[4]); options.setSendFrameMeta(sendFrameMeta); + boolean control = Boolean.parseBoolean(args[5]); + options.setControl(control); + return options; } diff --git a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java new file mode 100644 index 00000000..199fc8c1 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java @@ -0,0 +1,23 @@ +package com.genymobile.scrcpy; + +public final class StringUtils { + private StringUtils() { + // not instantiable + } + + @SuppressWarnings("checkstyle:MagicNumber") + public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { + int len = utf8.length; + if (len <= maxLength) { + return len; + } + len = maxLength; + // see UTF-8 encoding + while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) { + // the next byte is not the start of a new UTF-8 codepoint + // so if we would cut there, the character would be truncated + len--; + } + return len; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java new file mode 100644 index 00000000..a058a8bb --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -0,0 +1,44 @@ +package com.genymobile.scrcpy.wrappers; + +import android.content.ClipData; +import android.os.IInterface; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ClipboardManager { + private final IInterface manager; + private final Method getPrimaryClipMethod; + private final Method setPrimaryClipMethod; + + public ClipboardManager(IInterface manager) { + this.manager = manager; + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + public CharSequence getText() { + try { + ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell"); + if (clipData == null || clipData.getItemCount() == 0) { + return null; + } + return clipData.getItemAt(0).getText(); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + public void setText(CharSequence text) { + ClipData clipData = ClipData.newPlainText(null, text); + try { + setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell"); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new AssertionError(e); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index 3bcdc0e6..0b625c92 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -15,6 +15,7 @@ public final class ServiceManager { private InputManager inputManager; private PowerManager powerManager; private StatusBarManager statusBarManager; + private ClipboardManager clipboardManager; public ServiceManager() { try { @@ -68,4 +69,11 @@ public final class ServiceManager { } return statusBarManager; } + + public ClipboardManager getClipboardManager() { + if (clipboardManager == null) { + clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard")); + } + return clipboardManager; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 8f79f51f..74003b64 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.os.IInterface; import java.lang.reflect.InvocationTargetException; @@ -8,32 +10,42 @@ import java.lang.reflect.Method; public class StatusBarManager { private final IInterface manager; - private final Method expandNotificationsPanelMethod; - private final Method collapsePanelsMethod; + private Method expandNotificationsPanelMethod; + private Method collapsePanelsMethod; public StatusBarManager(IInterface manager) { this.manager = manager; - try { - expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); - collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); - } } public void expandNotificationsPanel() { + if (expandNotificationsPanelMethod == null) { + try { + expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); + } catch (NoSuchMethodException e) { + Ln.e("ServiceBarManager.expandNotificationsPanel() is not available on this device"); + return; + } + } try { expandNotificationsPanelMethod.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Cannot invoke ServiceBarManager.expandNotificationsPanel()", e); } } public void collapsePanels() { + if (collapsePanelsMethod == null) { + try { + collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); + } catch (NoSuchMethodException e) { + Ln.e("ServiceBarManager.collapsePanels() is not available on this device"); + return; + } + } try { collapsePanelsMethod.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Cannot invoke ServiceBarManager.collapsePanels()", e); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 85733867..bed21b3c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -10,6 +10,10 @@ public final class SurfaceControl { private static final Class CLASS; + // see + public static final int POWER_MODE_OFF = 0; + public static final int POWER_MODE_NORMAL = 2; + static { try { CLASS = Class.forName("android.view.SurfaceControl"); @@ -71,6 +75,22 @@ public final class SurfaceControl { } } + public static IBinder getBuiltInDisplay(int builtInDisplayId) { + try { + return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public static void setDisplayPowerMode(IBinder displayToken, int mode) { + try { + CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode); + } catch (Exception e) { + throw new AssertionError(e); + } + } + public static void destroyDisplay(IBinder displayToken) { try { CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java deleted file mode 100644 index 3e97096f..00000000 --- a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.genymobile.scrcpy; - -import android.view.KeyEvent; -import android.view.MotionEvent; - -import org.junit.Assert; -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; - - -public class ControlEventReaderTest { - - @Test - public void testParseKeycodeEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_KEYCODE); - dos.writeByte(KeyEvent.ACTION_UP); - dos.writeInt(KeyEvent.KEYCODE_ENTER); - dos.writeInt(KeyEvent.META_CTRL_ON); - byte[] packet = bos.toByteArray(); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); - - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); - Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); - Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); - Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - } - - @Test - public void testParseTextEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_TEXT); - byte[] text = "testé".getBytes(StandardCharsets.UTF_8); - dos.writeShort(text.length); - dos.write(text); - byte[] packet = bos.toByteArray(); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); - - Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType()); - Assert.assertEquals("testé", event.getText()); - } - - @Test - public void testParseLongTextEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_TEXT); - byte[] text = new byte[ControlEventReader.TEXT_MAX_LENGTH]; - Arrays.fill(text, (byte) 'a'); - dos.writeShort(text.length); - dos.write(text); - byte[] packet = bos.toByteArray(); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); - - Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType()); - Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); - } - - @Test - public void testParseMouseEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_KEYCODE); - dos.writeByte(MotionEvent.ACTION_DOWN); - dos.writeInt(MotionEvent.BUTTON_PRIMARY); - dos.writeInt(KeyEvent.META_CTRL_ON); - byte[] packet = bos.toByteArray(); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); - - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); - Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); - Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); - Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - } - - @Test - public void testMultiEvents() throws IOException { - ControlEventReader reader = new ControlEventReader(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - - dos.writeByte(ControlEvent.TYPE_KEYCODE); - dos.writeByte(KeyEvent.ACTION_UP); - dos.writeInt(KeyEvent.KEYCODE_ENTER); - dos.writeInt(KeyEvent.META_CTRL_ON); - - dos.writeByte(ControlEvent.TYPE_KEYCODE); - dos.writeByte(MotionEvent.ACTION_DOWN); - dos.writeInt(MotionEvent.BUTTON_PRIMARY); - dos.writeInt(KeyEvent.META_CTRL_ON); - - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - - ControlEvent event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); - Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); - Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); - Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - - event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); - Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); - Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); - Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - } - - @Test - public void testPartialEvents() throws IOException { - ControlEventReader reader = new ControlEventReader(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - - dos.writeByte(ControlEvent.TYPE_KEYCODE); - dos.writeByte(KeyEvent.ACTION_UP); - dos.writeInt(KeyEvent.KEYCODE_ENTER); - dos.writeInt(KeyEvent.META_CTRL_ON); - - dos.writeByte(ControlEvent.TYPE_KEYCODE); - dos.writeByte(MotionEvent.ACTION_DOWN); - - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - - ControlEvent event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); - Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); - Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); - Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - - event = reader.next(); - Assert.assertNull(event); // the event is not complete - - bos.reset(); - dos.writeInt(MotionEvent.BUTTON_PRIMARY); - dos.writeInt(KeyEvent.META_CTRL_ON); - packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - - // the event is now complete - event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); - Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); - Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); - Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - } -} diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java new file mode 100644 index 00000000..df1db1a6 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -0,0 +1,304 @@ +package com.genymobile.scrcpy; + +import android.view.KeyEvent; +import android.view.MotionEvent; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + + +public class ControlMessageReaderTest { + + @Test + public void testParseKeycodeEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(KeyEvent.ACTION_UP); + dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(KeyEvent.META_CTRL_ON); + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } + + @Test + public void testParseTextEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); + byte[] text = "testé".getBytes(StandardCharsets.UTF_8); + dos.writeShort(text.length); + dos.write(text); + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); + Assert.assertEquals("testé", event.getText()); + } + + @Test + public void testParseLongTextEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); + byte[] text = new byte[ControlMessageReader.TEXT_MAX_LENGTH]; + Arrays.fill(text, (byte) 'a'); + dos.writeShort(text.length); + dos.write(text); + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); + Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); + } + + @Test + public void testParseMouseEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(MotionEvent.ACTION_DOWN); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(KeyEvent.META_CTRL_ON); + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } + + @Test + @SuppressWarnings("checkstyle:MagicNumber") + public void testParseScrollEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT); + dos.writeInt(260); + dos.writeInt(1026); + dos.writeShort(1080); + dos.writeShort(1920); + dos.writeInt(1); + dos.writeInt(-1); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType()); + Assert.assertEquals(260, event.getPosition().getPoint().getX()); + Assert.assertEquals(1026, event.getPosition().getPoint().getY()); + Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); + Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); + Assert.assertEquals(1, event.getHScroll()); + Assert.assertEquals(-1, event.getVScroll()); + } + + @Test + public void testParseBackOrScreenOnEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); + } + + @Test + public void testParseExpandNotificationPanelEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType()); + } + + @Test + public void testParseCollapseNotificationPanelEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType()); + } + + @Test + public void testParseGetClipboardEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); + } + + @Test + public void testParseSetClipboardEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); + byte[] text = "testé".getBytes(StandardCharsets.UTF_8); + dos.writeShort(text.length); + dos.write(text); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals("testé", event.getText()); + } + + @Test + public void testParseSetScreenPowerMode() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); + dos.writeByte(Device.POWER_MODE_NORMAL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType()); + Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); + } + + @Test + public void testMultiEvents() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(KeyEvent.ACTION_UP); + dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(KeyEvent.META_CTRL_ON); + + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(MotionEvent.ACTION_DOWN); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(KeyEvent.META_CTRL_ON); + + byte[] packet = bos.toByteArray(); + reader.readFrom(new ByteArrayInputStream(packet)); + + ControlMessage event = reader.next(); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + event = reader.next(); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } + + @Test + public void testPartialEvents() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(KeyEvent.ACTION_UP); + dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(KeyEvent.META_CTRL_ON); + + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(MotionEvent.ACTION_DOWN); + + byte[] packet = bos.toByteArray(); + reader.readFrom(new ByteArrayInputStream(packet)); + + ControlMessage event = reader.next(); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + event = reader.next(); + Assert.assertNull(event); // the event is not complete + + bos.reset(); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(KeyEvent.META_CTRL_ON); + packet = bos.toByteArray(); + reader.readFrom(new ByteArrayInputStream(packet)); + + // the event is now complete + event = reader.next(); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java new file mode 100644 index 00000000..7d89ee64 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java @@ -0,0 +1,43 @@ +package com.genymobile.scrcpy; + +import org.junit.Assert; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +public class StringUtilsTest { + + @Test + @SuppressWarnings("checkstyle:MagicNumber") + public void testUtf8Truncate() { + String s = "aÉbÔc"; + byte[] utf8 = s.getBytes(StandardCharsets.UTF_8); + Assert.assertEquals(7, utf8.length); + + int count; + + count = StringUtils.getUtf8TruncationIndex(utf8, 1); + Assert.assertEquals(1, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 2); + Assert.assertEquals(1, count); // É is 2 bytes-wide + + count = StringUtils.getUtf8TruncationIndex(utf8, 3); + Assert.assertEquals(3, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 4); + Assert.assertEquals(4, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 5); + Assert.assertEquals(4, count); // Ô is 2 bytes-wide + + count = StringUtils.getUtf8TruncationIndex(utf8, 6); + Assert.assertEquals(6, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 7); + Assert.assertEquals(7, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 8); + Assert.assertEquals(7, count); // no more chars + } +}