Compare commits
37 Commits
v1.8
...
twosockets
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72c83b2ac2 | ||
|
|
7dcac5b8b3 | ||
|
|
f88345de0a | ||
|
|
2a1bdd0af3 | ||
|
|
2a7fee6c16 | ||
|
|
92f0266704 | ||
|
|
6cf134f31f | ||
|
|
b75f0e9427 | ||
|
|
5d473efeb5 | ||
|
|
a41dd6c79f | ||
|
|
c3779d8513 | ||
|
|
b3bd5f1b80 | ||
|
|
a920ba6471 | ||
|
|
3133d5d1c7 | ||
|
|
2dc1a59471 | ||
|
|
3068457b90 | ||
|
|
56f8e78f58 | ||
|
|
1630f923ef | ||
|
|
e443518ed9 | ||
|
|
eeb8e8420f | ||
|
|
39b5893c42 | ||
|
|
b941854c73 | ||
|
|
068253a3a2 | ||
|
|
c8338b2918 | ||
|
|
2837c6eaab | ||
|
|
668e54fd4b | ||
|
|
01664777c8 | ||
|
|
ffa8c66979 | ||
|
|
5254e585c6 | ||
|
|
66baf0f95b | ||
|
|
f11b0ec204 | ||
|
|
e2ef39fae5 | ||
|
|
3eda38e5fc | ||
|
|
a16cf95b8e | ||
|
|
71fd238b0a | ||
|
|
d795144a36 | ||
|
|
c287826f8e |
6
BUILD.md
6
BUILD.md
@@ -234,10 +234,10 @@ You can then [run](README.md#run) _scrcpy_.
|
|||||||
|
|
||||||
## Prebuilt server
|
## Prebuilt server
|
||||||
|
|
||||||
- [`scrcpy-server-v1.7.jar`][direct-scrcpy-server]
|
- [`scrcpy-server-v1.8.jar`][direct-scrcpy-server]
|
||||||
_(SHA-256: ee86ec8424f7dc50cacdf927312bdb46e0aa0d68611da584dc4b16d8057bc25e)_
|
_(SHA-256: 839055ef905903bf98ead1b9b8a127fe402b39ad657a81f9a914b2dbcb2ce5c0)_
|
||||||
|
|
||||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.7/scrcpy-server-v1.7.jar
|
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-server-v1.8.jar
|
||||||
|
|
||||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||||
configuration:
|
configuration:
|
||||||
|
|||||||
79
DEVELOP.md
79
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
|
args)`][main] method), compiled against the Android framework, and executed as
|
||||||
`shell` on the Android device.
|
`shell` on the Android device.
|
||||||
|
|
||||||
[main]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/Server.java#L61
|
[main]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/Server.java#L100
|
||||||
|
|
||||||
To run such a Java application, the classes must be [_dexed_][dex] (typically,
|
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
|
to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
|
||||||
@@ -65,8 +65,8 @@ They can be called using reflection though. The communication with hidden
|
|||||||
components is provided by [_wrappers_ classes][wrappers] and [aidl].
|
components is provided by [_wrappers_ classes][wrappers] and [aidl].
|
||||||
|
|
||||||
[hidden]: https://stackoverflow.com/a/31908373/1987178
|
[hidden]: https://stackoverflow.com/a/31908373/1987178
|
||||||
[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers
|
[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.0/server/src/main/aidl/android/view
|
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/aidl/android/view
|
||||||
|
|
||||||
|
|
||||||
### Threading
|
### Threading
|
||||||
@@ -89,9 +89,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
|
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).
|
stream to the provided output stream (the socket connected to the client).
|
||||||
|
|
||||||
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
|
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
|
||||||
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
|
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
|
||||||
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L63-L64
|
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L69-L70
|
||||||
|
|
||||||
On device [rotation], the codec, surface and display are reinitialized, and a
|
On device [rotation], the codec, surface and display are reinitialized, and a
|
||||||
new video stream is produced.
|
new video stream is produced.
|
||||||
@@ -105,8 +105,9 @@ because it avoids to send unnecessary frames, but there are drawbacks:
|
|||||||
Both problems are [solved][repeat] by the flag
|
Both problems are [solved][repeat] by the flag
|
||||||
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
|
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
|
||||||
|
|
||||||
[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L89-L92
|
[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.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126
|
[repeat]:
|
||||||
|
https://github.com/Genymobile/scrcpy/blob/v1.8/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
|
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
|
||||||
|
|
||||||
|
|
||||||
@@ -124,11 +125,11 @@ 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
|
the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our
|
||||||
[`InputManager` wrapper][inject-wrapper]).
|
[`InputManager` wrapper][inject-wrapper]).
|
||||||
|
|
||||||
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/EventController.java#L70
|
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/EventController.java#L66
|
||||||
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
|
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
|
||||||
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.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
|
[`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.0/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
|
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ Note that the client-server roles are expressed at the application level:
|
|||||||
- the server _serves_ video stream and handle requests from the client,
|
- the server _serves_ video stream and handle requests from the client,
|
||||||
- the client _controls_ the device through the server.
|
- the client _controls_ the device through the server.
|
||||||
|
|
||||||
However, the roles are inverted at the network level:
|
However, the roles are reversed at the network level:
|
||||||
|
|
||||||
- the client opens a server socket and listen on a port before starting the
|
- the client opens a server socket and listen on a port before starting the
|
||||||
server,
|
server,
|
||||||
@@ -162,6 +163,9 @@ However, the roles are inverted at the network level:
|
|||||||
This role inversion guarantees that the connection will not fail due to race
|
This role inversion guarantees that the connection will not fail due to race
|
||||||
conditions, and avoids polling.
|
conditions, and avoids polling.
|
||||||
|
|
||||||
|
_(Note that over TCP/IP, the roles are not reversed, due to a bug in `adb
|
||||||
|
reverse`. See commit [1038bad] and [issue #5].)_
|
||||||
|
|
||||||
Once the server is connected, it sends the device information (name and initial
|
Once the server is connected, it sends the device information (name and initial
|
||||||
screen dimensions). Thus, the client may init the window and renderer, before
|
screen dimensions). Thus, the client may init the window and renderer, before
|
||||||
the first frame is available.
|
the first frame is available.
|
||||||
@@ -169,6 +173,8 @@ the first frame is available.
|
|||||||
To minimize startup time, SDL initialization is performed while listening for
|
To minimize startup time, SDL initialization is performed while listening for
|
||||||
the connection from the server (see commit [90a46b4]).
|
the connection from the server (see commit [90a46b4]).
|
||||||
|
|
||||||
|
[1038bad]: https://github.com/Genymobile/scrcpy/commit/1038bad3850f18717a048a4d5c0f8110e54ee172
|
||||||
|
[issue #5]: https://github.com/Genymobile/scrcpy/issues/5
|
||||||
[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e
|
[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e
|
||||||
|
|
||||||
|
|
||||||
@@ -177,17 +183,25 @@ the connection from the server (see commit [90a46b4]).
|
|||||||
The client uses 3 threads:
|
The client uses 3 threads:
|
||||||
|
|
||||||
- the **main** thread, executing the SDL event loop,
|
- the **main** thread, executing the SDL event loop,
|
||||||
- the **decoder** thread, decoding video frames,
|
- 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 events_ to the server.
|
||||||
|
|
||||||
|
In addition, another thread can be started if necessary to handle APK
|
||||||
|
installation or file push requests (via drag&drop on the main window).
|
||||||
|
|
||||||
### Decoder
|
|
||||||
|
|
||||||
The [decoder] runs in a separate thread. It uses _libav_ to decode the H.264
|
|
||||||
stream from the socket, and notifies the main thread when a new frame is
|
|
||||||
available.
|
|
||||||
|
|
||||||
There are two [frames] simultaneously in memory:
|
### Stream
|
||||||
|
|
||||||
|
The video [stream] is received from the socket (connected to the server on the
|
||||||
|
device) in a separate thread.
|
||||||
|
|
||||||
|
If a [decoder] is present (i.e. `--no-display` is not set), then it uses _libav_
|
||||||
|
to decode the H.264 stream from the socket, and notifies the main thread when a
|
||||||
|
new frame is available.
|
||||||
|
|
||||||
|
There are two [frames][video_buffer] simultaneously in memory:
|
||||||
- the **decoding** frame, written by the decoder from the decoder thread,
|
- the **decoding** frame, written by the decoder from the decoder thread,
|
||||||
- the **rendering** frame, rendered in a texture from the main thread.
|
- the **rendering** frame, rendered in a texture from the main thread.
|
||||||
|
|
||||||
@@ -195,9 +209,23 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and
|
|||||||
rendering frame (with proper synchronization). Thus, it immediatly starts
|
rendering frame (with proper synchronization). Thus, it immediatly starts
|
||||||
to decode a new frame while the main thread renders the last one.
|
to decode a new frame while the main thread renders the last one.
|
||||||
|
|
||||||
[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/decoder.c
|
If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw
|
||||||
[frames]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/frames.h
|
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
|
||||||
|
|
||||||
|
```
|
||||||
|
+----------+ +----------+
|
||||||
|
---> | decoder | ---> | screen |
|
||||||
|
+---------+ / +----------+ +----------+
|
||||||
|
socket ---> | stream | ----
|
||||||
|
+---------+ \ +----------+
|
||||||
|
---> | recorder |
|
||||||
|
+----------+
|
||||||
|
```
|
||||||
|
|
||||||
### Controller
|
### Controller
|
||||||
|
|
||||||
@@ -211,10 +239,10 @@ 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
|
controller takes events from the queue, that it serializes and sends to the
|
||||||
client.
|
client.
|
||||||
|
|
||||||
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controller.h
|
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/controller.h
|
||||||
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controlevent.h
|
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/control_event.h
|
||||||
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/inputmanager.h
|
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/input_manager.h
|
||||||
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/convert.h
|
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/convert.h
|
||||||
|
|
||||||
|
|
||||||
### UI and event loop
|
### UI and event loop
|
||||||
@@ -225,9 +253,10 @@ thread.
|
|||||||
Events are handled in the [event loop], which either updates the [screen] or
|
Events are handled in the [event loop], which either updates the [screen] or
|
||||||
delegates to the [input manager][inputmanager].
|
delegates to the [input manager][inputmanager].
|
||||||
|
|
||||||
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c
|
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c
|
||||||
[event loop]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c#L38
|
[event loop]:
|
||||||
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/screen.h
|
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
|
||||||
|
|
||||||
|
|
||||||
## Hack
|
## Hack
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -1,4 +1,4 @@
|
|||||||
# scrcpy (v1.7)
|
# scrcpy (v1.8)
|
||||||
|
|
||||||
This application provides display and control of Android devices connected on
|
This application provides display and control of Android devices connected on
|
||||||
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
|
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
|
||||||
@@ -29,12 +29,10 @@ control it using keyboard and mouse.
|
|||||||
On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
|
On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
|
||||||
it's not that hard.
|
it's not that hard.
|
||||||
|
|
||||||
For Arch Linux, two [AUR] packages have been created by users:
|
For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link].
|
||||||
|
|
||||||
- [`scrcpy`](https://aur.archlinux.org/packages/scrcpy/)
|
|
||||||
- [`scrcpy-prebuiltserver`](https://aur.archlinux.org/packages/scrcpy-prebuiltserver/)
|
|
||||||
|
|
||||||
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||||
|
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
|
||||||
|
|
||||||
For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
|
For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
|
||||||
|
|
||||||
@@ -47,13 +45,13 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
|
|||||||
For Windows, for simplicity, prebuilt archives with all the dependencies
|
For Windows, for simplicity, prebuilt archives with all the dependencies
|
||||||
(including `adb`) are available:
|
(including `adb`) are available:
|
||||||
|
|
||||||
- [`scrcpy-win32-v1.7.zip`][direct-win32]
|
- [`scrcpy-win32-v1.8.zip`][direct-win32]
|
||||||
_(SHA-256: 98ae36f2da0b8212c07066fd93139650554274f863d4cee0781501a0c84f7c23)_
|
_(SHA-256: c0c29ed1c66deaa73bdadacd09e598aafb3a117929cf7a314cce1cc45e34de53)_
|
||||||
- [`scrcpy-win64-v1.7.zip`][direct-win64]
|
- [`scrcpy-win64-v1.8.zip`][direct-win64]
|
||||||
_(SHA-256: b41416547521062f19e3f3f539e89a70e713bd086e69ef1b29c128993f7aa462)_
|
_(SHA-256: 9cc980d07bd8f036ae4e91d0bc6fc3281d7fa8f9752d4913b643c0fb72a19fb7)_
|
||||||
|
|
||||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.7/scrcpy-win32-v1.7.zip
|
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win32-v1.8.zip
|
||||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.7/scrcpy-win64-v1.7.zip
|
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win64-v1.8.zip
|
||||||
|
|
||||||
You can also [build the app manually][BUILD].
|
You can also [build the app manually][BUILD].
|
||||||
|
|
||||||
@@ -171,6 +169,7 @@ To disable mirroring while recording:
|
|||||||
scrcpy --no-display --record file.mp4
|
scrcpy --no-display --record file.mp4
|
||||||
scrcpy -Nr file.mkv
|
scrcpy -Nr file.mkv
|
||||||
# interrupt recording with Ctrl+C
|
# interrupt recording with Ctrl+C
|
||||||
|
# Ctrl+C does not terminate properly on Windows, so disconnect the device
|
||||||
```
|
```
|
||||||
|
|
||||||
"Skipped frames" are recorded, even if they are not displayed in real time (for
|
"Skipped frames" are recorded, even if they are not displayed in real time (for
|
||||||
@@ -295,6 +294,19 @@ _¹Double-click on black borders to remove them._
|
|||||||
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
||||||
|
|
||||||
|
|
||||||
|
## Custom paths
|
||||||
|
|
||||||
|
To use a specific _adb_ binary, configure its path in the environment variable
|
||||||
|
`ADB`:
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
|
||||||
|
|
||||||
|
|
||||||
## Why _scrcpy_?
|
## Why _scrcpy_?
|
||||||
|
|
||||||
A colleague challenged me to find a name as unpronounceable as [gnirehtet].
|
A colleague challenged me to find a name as unpronounceable as [gnirehtet].
|
||||||
|
|||||||
@@ -143,18 +143,34 @@ else
|
|||||||
link_args = []
|
link_args = []
|
||||||
endif
|
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
|
||||||
|
|
||||||
tests = [
|
tests = [
|
||||||
['test_control_event_queue', ['tests/test_control_event_queue.c', 'src/control_event.c']],
|
['test_control_event_queue', [
|
||||||
['test_control_event_serialize', ['tests/test_control_event_serialize.c', 'src/control_event.c']],
|
'tests/test_control_event_queue.c',
|
||||||
['test_strutil', ['tests/test_strutil.c', 'src/str_util.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'
|
||||||
|
]],
|
||||||
]
|
]
|
||||||
|
|
||||||
foreach t : tests
|
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)
|
test(t[0], exe)
|
||||||
endforeach
|
endforeach
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "command.h"
|
#include "command.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -20,15 +21,52 @@ get_adb_command(void) {
|
|||||||
return adb_command;
|
return adb_command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serialize argv to string "[arg1], [arg2], [arg3]"
|
||||||
|
static size_t
|
||||||
|
argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
|
||||||
|
size_t idx = 0;
|
||||||
|
bool first = true;
|
||||||
|
while (*argv) {
|
||||||
|
const char *arg = *argv;
|
||||||
|
size_t len = strlen(arg);
|
||||||
|
// count space for "[], ...\0"
|
||||||
|
if (idx + len + 8 >= bufsize) {
|
||||||
|
// not enough space, truncate
|
||||||
|
assert(idx < bufsize - 4);
|
||||||
|
memcpy(&buf[idx], "...", 3);
|
||||||
|
idx += 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
buf[idx++] = ',';
|
||||||
|
buf[idx++] = ' ';
|
||||||
|
}
|
||||||
|
buf[idx++] = '[';
|
||||||
|
memcpy(&buf[idx], arg, len);
|
||||||
|
idx += len;
|
||||||
|
buf[idx++] = ']';
|
||||||
|
argv++;
|
||||||
|
}
|
||||||
|
assert(idx < bufsize);
|
||||||
|
buf[idx] = '\0';
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
show_adb_err_msg(enum process_result err) {
|
show_adb_err_msg(enum process_result err, const char *const argv[]) {
|
||||||
|
char buf[512];
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case PROCESS_ERROR_GENERIC:
|
case PROCESS_ERROR_GENERIC:
|
||||||
LOGE("Failed to execute adb");
|
argv_to_string(argv, buf, sizeof(buf));
|
||||||
|
LOGE("Failed to execute: %s", buf);
|
||||||
break;
|
break;
|
||||||
case PROCESS_ERROR_MISSING_BINARY:
|
case PROCESS_ERROR_MISSING_BINARY:
|
||||||
LOGE("'adb' command not found (make it accessible from your PATH "
|
argv_to_string(argv, buf, sizeof(buf));
|
||||||
"or define its full path in the ADB environment variable)");
|
LOGE("Command not found: %s", buf);
|
||||||
|
LOGE("(make 'adb' accessible from your PATH or define its full"
|
||||||
|
"path in the ADB environment variable)");
|
||||||
break;
|
break;
|
||||||
case PROCESS_SUCCESS:
|
case PROCESS_SUCCESS:
|
||||||
/* do nothing */
|
/* do nothing */
|
||||||
@@ -37,7 +75,7 @@ show_adb_err_msg(enum process_result err) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
process_t
|
process_t
|
||||||
adb_execute(const char *serial, const char *const adb_cmd[], int len) {
|
adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
|
||||||
const char *cmd[len + 4];
|
const char *cmd[len + 4];
|
||||||
int i;
|
int i;
|
||||||
process_t process;
|
process_t process;
|
||||||
@@ -54,7 +92,7 @@ adb_execute(const char *serial, const char *const adb_cmd[], int len) {
|
|||||||
cmd[len + i] = NULL;
|
cmd[len + i] = NULL;
|
||||||
enum process_result r = cmd_execute(cmd[0], cmd, &process);
|
enum process_result r = cmd_execute(cmd[0], cmd, &process);
|
||||||
if (r != PROCESS_SUCCESS) {
|
if (r != PROCESS_SUCCESS) {
|
||||||
show_adb_err_msg(r);
|
show_adb_err_msg(r, cmd);
|
||||||
return PROCESS_NONE;
|
return PROCESS_NONE;
|
||||||
}
|
}
|
||||||
return process;
|
return process;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ bool
|
|||||||
cmd_simple_wait(process_t pid, exit_code_t *exit_code);
|
cmd_simple_wait(process_t pid, exit_code_t *exit_code);
|
||||||
|
|
||||||
process_t
|
process_t
|
||||||
adb_execute(const char *serial, const char *const adb_cmd[], int len);
|
adb_execute(const char *serial, const char *const adb_cmd[], size_t len);
|
||||||
|
|
||||||
process_t
|
process_t
|
||||||
adb_forward(const char *serial, uint16_t local_port,
|
adb_forward(const char *serial, uint16_t local_port,
|
||||||
|
|||||||
@@ -43,4 +43,9 @@
|
|||||||
# define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
|
# define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 8)
|
||||||
|
// <https://hg.libsdl.org/SDL/rev/dfde5d3f9781>
|
||||||
|
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -81,9 +81,9 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
|
|||||||
MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE);
|
MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE);
|
||||||
MAP(SDLK_BACKSPACE, AKEYCODE_DEL);
|
MAP(SDLK_BACKSPACE, AKEYCODE_DEL);
|
||||||
MAP(SDLK_TAB, AKEYCODE_TAB);
|
MAP(SDLK_TAB, AKEYCODE_TAB);
|
||||||
MAP(SDLK_HOME, AKEYCODE_HOME);
|
|
||||||
MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP);
|
MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP);
|
||||||
MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL);
|
MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL);
|
||||||
|
MAP(SDLK_HOME, AKEYCODE_MOVE_HOME);
|
||||||
MAP(SDLK_END, AKEYCODE_MOVE_END);
|
MAP(SDLK_END, AKEYCODE_MOVE_END);
|
||||||
MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN);
|
MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN);
|
||||||
MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT);
|
MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT);
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
|
|
||||||
struct request {
|
struct request {
|
||||||
file_handler_action_t action;
|
file_handler_action_t action;
|
||||||
const char *file;
|
char *file;
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct request *
|
static struct request *
|
||||||
request_new(file_handler_action_t action, const char *file) {
|
request_new(file_handler_action_t action, char *file) {
|
||||||
struct request *req = SDL_malloc(sizeof(*req));
|
struct request *req = SDL_malloc(sizeof(*req));
|
||||||
if (!req) {
|
if (!req) {
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -29,8 +29,8 @@ request_free(struct request *req) {
|
|||||||
if (!req) {
|
if (!req) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SDL_free((void *) req->file);
|
SDL_free(req->file);
|
||||||
SDL_free((void *) req);
|
SDL_free(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
@@ -121,7 +121,7 @@ file_handler_destroy(struct file_handler *file_handler) {
|
|||||||
SDL_DestroyCond(file_handler->event_cond);
|
SDL_DestroyCond(file_handler->event_cond);
|
||||||
SDL_DestroyMutex(file_handler->mutex);
|
SDL_DestroyMutex(file_handler->mutex);
|
||||||
request_queue_destroy(&file_handler->queue);
|
request_queue_destroy(&file_handler->queue);
|
||||||
SDL_free((void *) file_handler->serial);
|
SDL_free(file_handler->serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
static process_t
|
static process_t
|
||||||
@@ -137,7 +137,7 @@ push_file(const char *serial, const char *file) {
|
|||||||
bool
|
bool
|
||||||
file_handler_request(struct file_handler *file_handler,
|
file_handler_request(struct file_handler *file_handler,
|
||||||
file_handler_action_t action,
|
file_handler_action_t action,
|
||||||
const char *file) {
|
char *file) {
|
||||||
bool res;
|
bool res;
|
||||||
|
|
||||||
// start file_handler if it's used for the first time
|
// start file_handler if it's used for the first time
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ struct request_queue {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct file_handler {
|
struct file_handler {
|
||||||
const char *serial;
|
char *serial;
|
||||||
SDL_Thread *thread;
|
SDL_Thread *thread;
|
||||||
SDL_mutex *mutex;
|
SDL_mutex *mutex;
|
||||||
SDL_cond *event_cond;
|
SDL_cond *event_cond;
|
||||||
@@ -46,9 +46,10 @@ file_handler_stop(struct file_handler *file_handler);
|
|||||||
void
|
void
|
||||||
file_handler_join(struct file_handler *file_handler);
|
file_handler_join(struct file_handler *file_handler);
|
||||||
|
|
||||||
|
// take ownership of file, and will SDL_free() it
|
||||||
bool
|
bool
|
||||||
file_handler_request(struct file_handler *file_handler,
|
file_handler_request(struct file_handler *file_handler,
|
||||||
file_handler_action_t action,
|
file_handler_action_t action,
|
||||||
const char *file);
|
char *file);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
#include "compat.h"
|
#include "compat.h"
|
||||||
@@ -104,7 +105,6 @@ static void usage(const char *arg0) {
|
|||||||
" resize window to remove black borders\n"
|
" resize window to remove black borders\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+h\n"
|
" Ctrl+h\n"
|
||||||
" Home\n"
|
|
||||||
" Middle-click\n"
|
" Middle-click\n"
|
||||||
" click on HOME\n"
|
" click on HOME\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
#include "compat.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "decoder.h"
|
#include "decoder.h"
|
||||||
#include "device.h"
|
#include "device.h"
|
||||||
@@ -68,6 +69,13 @@ sdl_init_and_configure(bool display) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
|
||||||
|
// Disable compositor bypassing on X11
|
||||||
|
if (!SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0")) {
|
||||||
|
LOGW("Could not disable X11 compositor bypass");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Do not disable the screensaver when scrcpy is running
|
// Do not disable the screensaver when scrcpy is running
|
||||||
SDL_EnableScreenSaver();
|
SDL_EnableScreenSaver();
|
||||||
|
|
||||||
@@ -282,8 +290,7 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
goto finally_destroy_server;
|
goto finally_destroy_server;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket_t device_socket = server_connect_to(&server);
|
if (!server_connect_to(&server)) {
|
||||||
if (device_socket == INVALID_SOCKET) {
|
|
||||||
server_stop(&server);
|
server_stop(&server);
|
||||||
ret = false;
|
ret = false;
|
||||||
goto finally_destroy_server;
|
goto finally_destroy_server;
|
||||||
@@ -295,7 +302,7 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
// screenrecord does not send frames when the screen content does not
|
// screenrecord does not send frames when the screen content does not
|
||||||
// change therefore, we transmit the screen size before the video stream,
|
// change therefore, we transmit the screen size before the video stream,
|
||||||
// to be able to init the window immediately
|
// to be able to init the window immediately
|
||||||
if (!device_read_info(device_socket, device_name, &frame_size)) {
|
if (!device_read_info(server.video_socket, device_name, &frame_size)) {
|
||||||
server_stop(&server);
|
server_stop(&server);
|
||||||
ret = false;
|
ret = false;
|
||||||
goto finally_destroy_server;
|
goto finally_destroy_server;
|
||||||
@@ -334,7 +341,7 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
|
|
||||||
av_log_set_callback(av_log_callback);
|
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
|
// now we consumed the header values, the socket receives the video stream
|
||||||
// start the stream
|
// start the stream
|
||||||
@@ -346,7 +353,7 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
|
|
||||||
if (display) {
|
if (display) {
|
||||||
if (control) {
|
if (control) {
|
||||||
if (!controller_init(&controller, device_socket)) {
|
if (!controller_init(&controller, server.control_socket)) {
|
||||||
ret = false;
|
ret = false;
|
||||||
goto finally_stop_stream;
|
goto finally_stop_stream;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,13 +177,12 @@ screen_init_rendering(struct screen *screen, const char *device_name,
|
|||||||
}
|
}
|
||||||
|
|
||||||
SDL_Surface *icon = read_xpm(icon_xpm);
|
SDL_Surface *icon = read_xpm(icon_xpm);
|
||||||
if (!icon) {
|
if (icon) {
|
||||||
LOGE("Could not load icon: %s", SDL_GetError());
|
|
||||||
screen_destroy(screen);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
SDL_SetWindowIcon(screen->window, icon);
|
SDL_SetWindowIcon(screen->window, icon);
|
||||||
SDL_FreeSurface(icon);
|
SDL_FreeSurface(icon);
|
||||||
|
} else {
|
||||||
|
LOGW("Could not load icon");
|
||||||
|
}
|
||||||
|
|
||||||
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width,
|
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width,
|
||||||
frame_size.height);
|
frame_size.height);
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ connect_and_read_byte(uint16_t port) {
|
|||||||
char byte;
|
char byte;
|
||||||
// the connection may succeed even if the server behind the "adb tunnel"
|
// the connection may succeed even if the server behind the "adb tunnel"
|
||||||
// is not listening, so read one byte to detect a working connection
|
// 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
|
// the server is not listening yet behind the adb tunnel
|
||||||
return INVALID_SOCKET;
|
return INVALID_SOCKET;
|
||||||
}
|
}
|
||||||
@@ -210,7 +210,7 @@ server_start(struct server *server, const char *serial,
|
|||||||
close_socket(&server->server_socket);
|
close_socket(&server->server_socket);
|
||||||
}
|
}
|
||||||
disable_tunnel(server);
|
disable_tunnel(server);
|
||||||
SDL_free((void *) server->serial);
|
SDL_free(server->serial);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,31 +219,44 @@ server_start(struct server *server, const char *serial,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket_t
|
bool
|
||||||
server_connect_to(struct server *server) {
|
server_connect_to(struct server *server) {
|
||||||
if (!server->tunnel_forward) {
|
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 {
|
} else {
|
||||||
uint32_t attempts = 100;
|
uint32_t attempts = 100;
|
||||||
uint32_t delay = 100; // ms
|
uint32_t delay = 100; // ms
|
||||||
server->device_socket = connect_to_server(server->local_port, attempts,
|
server->video_socket =
|
||||||
delay);
|
connect_to_server(server->local_port, attempts, delay);
|
||||||
|
if (server->video_socket == INVALID_SOCKET) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server->device_socket == INVALID_SOCKET) {
|
// we know that the device is listening, we don't need several attempts
|
||||||
return INVALID_SOCKET;
|
server->control_socket =
|
||||||
|
net_connect(IPV4_LOCALHOST, server->local_port);
|
||||||
|
if (server->control_socket == INVALID_SOCKET) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!server->tunnel_forward) {
|
|
||||||
// we don't need the server socket anymore
|
|
||||||
close_socket(&server->server_socket);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// we don't need the adb tunnel anymore
|
// we don't need the adb tunnel anymore
|
||||||
disable_tunnel(server); // ignore failure
|
disable_tunnel(server); // ignore failure
|
||||||
server->tunnel_enabled = false;
|
server->tunnel_enabled = false;
|
||||||
|
|
||||||
return server->device_socket;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -268,8 +281,11 @@ server_destroy(struct server *server) {
|
|||||||
if (server->server_socket != INVALID_SOCKET) {
|
if (server->server_socket != INVALID_SOCKET) {
|
||||||
close_socket(&server->server_socket);
|
close_socket(&server->server_socket);
|
||||||
}
|
}
|
||||||
if (server->device_socket != INVALID_SOCKET) {
|
if (server->video_socket != INVALID_SOCKET) {
|
||||||
close_socket(&server->device_socket);
|
close_socket(&server->video_socket);
|
||||||
|
}
|
||||||
|
if (server->control_socket != INVALID_SOCKET) {
|
||||||
|
close_socket(&server->control_socket);
|
||||||
}
|
}
|
||||||
SDL_free(server->serial);
|
SDL_free(server->serial);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ struct server {
|
|||||||
char *serial;
|
char *serial;
|
||||||
process_t process;
|
process_t process;
|
||||||
socket_t server_socket; // only used if !tunnel_forward
|
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;
|
uint16_t local_port;
|
||||||
bool tunnel_enabled;
|
bool tunnel_enabled;
|
||||||
bool tunnel_forward; // use "adb forward" instead of "adb reverse"
|
bool tunnel_forward; // use "adb forward" instead of "adb reverse"
|
||||||
@@ -22,7 +23,8 @@ struct server {
|
|||||||
.serial = NULL, \
|
.serial = NULL, \
|
||||||
.process = PROCESS_NONE, \
|
.process = PROCESS_NONE, \
|
||||||
.server_socket = INVALID_SOCKET, \
|
.server_socket = INVALID_SOCKET, \
|
||||||
.device_socket = INVALID_SOCKET, \
|
.video_socket = INVALID_SOCKET, \
|
||||||
|
.control_socket = INVALID_SOCKET, \
|
||||||
.local_port = 0, \
|
.local_port = 0, \
|
||||||
.tunnel_enabled = false, \
|
.tunnel_enabled = false, \
|
||||||
.tunnel_forward = false, \
|
.tunnel_forward = false, \
|
||||||
@@ -40,7 +42,7 @@ server_start(struct server *server, const char *serial,
|
|||||||
const char *crop, bool send_frame_meta);
|
const char *crop, bool send_frame_meta);
|
||||||
|
|
||||||
// block until the communication with the server is established
|
// block until the communication with the server is established
|
||||||
socket_t
|
bool
|
||||||
server_connect_to(struct server *server);
|
server_connect_to(struct server *server);
|
||||||
|
|
||||||
// disconnect and kill the server process
|
// disconnect and kill the server process
|
||||||
|
|||||||
@@ -105,6 +105,10 @@ read_xpm(char *xpm[]) {
|
|||||||
width, height,
|
width, height,
|
||||||
32, 4 * width,
|
32, 4 * width,
|
||||||
rmask, gmask, bmask, amask);
|
rmask, gmask, bmask, amask);
|
||||||
|
if (!surface) {
|
||||||
|
LOGE("Could not create icon surface");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
// make the surface own the raw pixels
|
// make the surface own the raw pixels
|
||||||
surface->flags &= ~SDL_PREALLOC;
|
surface->flags &= ~SDL_PREALLOC;
|
||||||
return surface;
|
return surface;
|
||||||
|
|||||||
@@ -5,21 +5,21 @@
|
|||||||
|
|
||||||
static void test_control_event_queue_empty(void) {
|
static void test_control_event_queue_empty(void) {
|
||||||
struct control_event_queue queue;
|
struct control_event_queue queue;
|
||||||
SDL_bool init_ok = control_event_queue_init(&queue);
|
bool init_ok = control_event_queue_init(&queue);
|
||||||
assert(init_ok);
|
assert(init_ok);
|
||||||
|
|
||||||
assert(control_event_queue_is_empty(&queue));
|
assert(control_event_queue_is_empty(&queue));
|
||||||
|
|
||||||
struct control_event dummy_event;
|
struct control_event dummy_event;
|
||||||
SDL_bool push_ok = control_event_queue_push(&queue, &dummy_event);
|
bool push_ok = control_event_queue_push(&queue, &dummy_event);
|
||||||
assert(push_ok);
|
assert(push_ok);
|
||||||
assert(!control_event_queue_is_empty(&queue));
|
assert(!control_event_queue_is_empty(&queue));
|
||||||
|
|
||||||
SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event);
|
bool take_ok = control_event_queue_take(&queue, &dummy_event);
|
||||||
assert(take_ok);
|
assert(take_ok);
|
||||||
assert(control_event_queue_is_empty(&queue));
|
assert(control_event_queue_is_empty(&queue));
|
||||||
|
|
||||||
SDL_bool take_empty_ok = control_event_queue_take(&queue, &dummy_event);
|
bool take_empty_ok = control_event_queue_take(&queue, &dummy_event);
|
||||||
assert(!take_empty_ok); // the queue is empty
|
assert(!take_empty_ok); // the queue is empty
|
||||||
|
|
||||||
control_event_queue_destroy(&queue);
|
control_event_queue_destroy(&queue);
|
||||||
@@ -27,7 +27,7 @@ static void test_control_event_queue_empty(void) {
|
|||||||
|
|
||||||
static void test_control_event_queue_full(void) {
|
static void test_control_event_queue_full(void) {
|
||||||
struct control_event_queue queue;
|
struct control_event_queue queue;
|
||||||
SDL_bool init_ok = control_event_queue_init(&queue);
|
bool init_ok = control_event_queue_init(&queue);
|
||||||
assert(init_ok);
|
assert(init_ok);
|
||||||
|
|
||||||
assert(!control_event_queue_is_full(&queue));
|
assert(!control_event_queue_is_full(&queue));
|
||||||
@@ -36,7 +36,7 @@ static void test_control_event_queue_full(void) {
|
|||||||
// fill the queue
|
// fill the queue
|
||||||
while (control_event_queue_push(&queue, &dummy_event));
|
while (control_event_queue_push(&queue, &dummy_event));
|
||||||
|
|
||||||
SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event);
|
bool take_ok = control_event_queue_take(&queue, &dummy_event);
|
||||||
assert(take_ok);
|
assert(take_ok);
|
||||||
assert(!control_event_queue_is_full(&queue));
|
assert(!control_event_queue_is_full(&queue));
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ static void test_control_event_queue_full(void) {
|
|||||||
|
|
||||||
static void test_control_event_queue_push_take(void) {
|
static void test_control_event_queue_push_take(void) {
|
||||||
struct control_event_queue queue;
|
struct control_event_queue queue;
|
||||||
SDL_bool init_ok = control_event_queue_init(&queue);
|
bool init_ok = control_event_queue_init(&queue);
|
||||||
assert(init_ok);
|
assert(init_ok);
|
||||||
|
|
||||||
struct control_event event = {
|
struct control_event event = {
|
||||||
@@ -57,7 +57,7 @@ static void test_control_event_queue_push_take(void) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
SDL_bool push1_ok = control_event_queue_push(&queue, &event);
|
bool push1_ok = control_event_queue_push(&queue, &event);
|
||||||
assert(push1_ok);
|
assert(push1_ok);
|
||||||
|
|
||||||
event = (struct control_event) {
|
event = (struct control_event) {
|
||||||
@@ -67,11 +67,11 @@ static void test_control_event_queue_push_take(void) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
SDL_bool push2_ok = control_event_queue_push(&queue, &event);
|
bool push2_ok = control_event_queue_push(&queue, &event);
|
||||||
assert(push2_ok);
|
assert(push2_ok);
|
||||||
|
|
||||||
// overwrite event
|
// overwrite event
|
||||||
SDL_bool take1_ok = control_event_queue_take(&queue, &event);
|
bool take1_ok = control_event_queue_take(&queue, &event);
|
||||||
assert(take1_ok);
|
assert(take1_ok);
|
||||||
assert(event.type == CONTROL_EVENT_TYPE_KEYCODE);
|
assert(event.type == CONTROL_EVENT_TYPE_KEYCODE);
|
||||||
assert(event.keycode_event.action == AKEY_EVENT_ACTION_DOWN);
|
assert(event.keycode_event.action == AKEY_EVENT_ACTION_DOWN);
|
||||||
@@ -79,7 +79,7 @@ static void test_control_event_queue_push_take(void) {
|
|||||||
assert(event.keycode_event.metastate == (AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON));
|
assert(event.keycode_event.metastate == (AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON));
|
||||||
|
|
||||||
// overwrite event
|
// overwrite event
|
||||||
SDL_bool take2_ok = control_event_queue_take(&queue, &event);
|
bool take2_ok = control_event_queue_take(&queue, &event);
|
||||||
assert(take2_ok);
|
assert(take2_ok);
|
||||||
assert(event.type == CONTROL_EVENT_TYPE_TEXT);
|
assert(event.type == CONTROL_EVENT_TYPE_TEXT);
|
||||||
assert(!strcmp(event.text_event.text, "abc"));
|
assert(!strcmp(event.text_event.text, "abc"));
|
||||||
|
|||||||
@@ -49,13 +49,14 @@ static void test_serialize_text_event(void) {
|
|||||||
static void test_serialize_long_text_event(void) {
|
static void test_serialize_long_text_event(void) {
|
||||||
struct control_event event;
|
struct control_event event;
|
||||||
event.type = CONTROL_EVENT_TYPE_TEXT;
|
event.type = CONTROL_EVENT_TYPE_TEXT;
|
||||||
char text[TEXT_MAX_LENGTH];
|
char text[TEXT_MAX_LENGTH + 1];
|
||||||
memset(text, 'a', sizeof(text));
|
memset(text, 'a', sizeof(text));
|
||||||
|
text[TEXT_MAX_LENGTH] = '\0';
|
||||||
event.text_event.text = text;
|
event.text_event.text = text;
|
||||||
|
|
||||||
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
|
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
|
||||||
int size = control_event_serialize(&event, buf);
|
int size = control_event_serialize(&event, buf);
|
||||||
assert(size == 3 + sizeof(text));
|
assert(size == 3 + TEXT_MAX_LENGTH);
|
||||||
|
|
||||||
unsigned char expected[3 + TEXT_MAX_LENGTH];
|
unsigned char expected[3 + TEXT_MAX_LENGTH];
|
||||||
expected[0] = 0x01; // CONTROL_EVENT_TYPE_KEYCODE
|
expected[0] = 0x01; // CONTROL_EVENT_TYPE_KEYCODE
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
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
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
#Mon Jun 04 11:48:32 CEST 2018
|
#Thu Apr 18 11:45:59 CEST 2019
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
||||||
|
|||||||
13
release.sh
13
release.sh
@@ -1,13 +1,22 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# build and test locally
|
# test locally
|
||||||
|
TESTDIR=build_test
|
||||||
|
rm -rf "$TESTDIR"
|
||||||
|
# run client tests with ASAN enabled
|
||||||
|
meson "$TESTDIR" -Db_sanitize=address
|
||||||
|
ninja -C"$TESTDIR" test
|
||||||
|
|
||||||
|
# test server
|
||||||
|
GRADLE=${GRADLE:-./gradlew}
|
||||||
|
$GRADLE -p server check
|
||||||
|
|
||||||
BUILDDIR=build_release
|
BUILDDIR=build_release
|
||||||
rm -rf "$BUILDDIR"
|
rm -rf "$BUILDDIR"
|
||||||
meson "$BUILDDIR" --buildtype release --strip -Db_lto=true
|
meson "$BUILDDIR" --buildtype release --strip -Db_lto=true
|
||||||
cd "$BUILDDIR"
|
cd "$BUILDDIR"
|
||||||
ninja
|
ninja
|
||||||
ninja test
|
|
||||||
cd -
|
cd -
|
||||||
|
|
||||||
# build Windows releases
|
# build Windows releases
|
||||||
|
|||||||
@@ -16,16 +16,20 @@ public final class DesktopConnection implements Closeable {
|
|||||||
|
|
||||||
private static final String SOCKET_NAME = "scrcpy";
|
private static final String SOCKET_NAME = "scrcpy";
|
||||||
|
|
||||||
private final LocalSocket socket;
|
private final LocalSocket videoSocket;
|
||||||
private final InputStream inputStream;
|
private final FileDescriptor videoFd;
|
||||||
private final FileDescriptor fd;
|
|
||||||
|
private final LocalSocket controlSocket;
|
||||||
|
private final InputStream controlInputStream;
|
||||||
|
|
||||||
|
|
||||||
private final ControlEventReader reader = new ControlEventReader();
|
private final ControlEventReader reader = new ControlEventReader();
|
||||||
|
|
||||||
private DesktopConnection(LocalSocket socket) throws IOException {
|
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
|
||||||
this.socket = socket;
|
this.videoSocket = videoSocket;
|
||||||
inputStream = socket.getInputStream();
|
this.controlSocket = controlSocket;
|
||||||
fd = socket.getFileDescriptor();
|
controlInputStream = controlSocket.getInputStream();
|
||||||
|
videoFd = videoSocket.getFileDescriptor();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LocalSocket connect(String abstractName) throws IOException {
|
private static LocalSocket connect(String abstractName) throws IOException {
|
||||||
@@ -44,25 +48,46 @@ public final class DesktopConnection implements Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
|
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
|
||||||
LocalSocket socket;
|
LocalSocket videoSocket;
|
||||||
|
LocalSocket controlSocket;
|
||||||
if (tunnelForward) {
|
if (tunnelForward) {
|
||||||
socket = listenAndAccept(SOCKET_NAME);
|
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
|
||||||
|
try {
|
||||||
|
videoSocket = localServerSocket.accept();
|
||||||
// send one byte so the client may read() to detect a connection error
|
// send one byte so the client may read() to detect a connection error
|
||||||
socket.getOutputStream().write(0);
|
videoSocket.getOutputStream().write(0);
|
||||||
|
try {
|
||||||
|
controlSocket = localServerSocket.accept();
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
videoSocket.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
localServerSocket.close();
|
||||||
|
}
|
||||||
} else {
|
} 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();
|
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||||
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
socket.shutdownInput();
|
videoSocket.shutdownInput();
|
||||||
socket.shutdownOutput();
|
videoSocket.shutdownOutput();
|
||||||
socket.close();
|
videoSocket.close();
|
||||||
|
controlSocket.shutdownInput();
|
||||||
|
controlSocket.shutdownOutput();
|
||||||
|
controlSocket.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
@SuppressWarnings("checkstyle:MagicNumber")
|
||||||
@@ -78,17 +103,17 @@ public final class DesktopConnection implements Closeable {
|
|||||||
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
|
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
|
||||||
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
|
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
|
||||||
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
|
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() {
|
public FileDescriptor getVideoFd() {
|
||||||
return fd;
|
return videoFd;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ControlEvent receiveControlEvent() throws IOException {
|
public ControlEvent receiveControlEvent() throws IOException {
|
||||||
ControlEvent event = reader.next();
|
ControlEvent event = reader.next();
|
||||||
while (event == null) {
|
while (event == null) {
|
||||||
reader.readFrom(inputStream);
|
reader.readFrom(controlInputStream);
|
||||||
event = reader.next();
|
event = reader.next();
|
||||||
}
|
}
|
||||||
return event;
|
return event;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
|
||||||
|
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
@@ -105,13 +104,16 @@ public class EventController {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectText(String text) {
|
private int injectText(String text) {
|
||||||
|
int successCount = 0;
|
||||||
for (char c : text.toCharArray()) {
|
for (char c : text.toCharArray()) {
|
||||||
if (!injectChar(c)) {
|
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) {
|
private boolean injectMouse(int action, int buttons, Position position) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import java.io.FileDescriptor;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
public class IO {
|
public final class IO {
|
||||||
private IO() {
|
private IO() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,13 @@ public final class Ln {
|
|||||||
if (isEnabled(Level.ERROR)) {
|
if (isEnabled(Level.ERROR)) {
|
||||||
Log.e(TAG, message, throwable);
|
Log.e(TAG, message, throwable);
|
||||||
System.out.println("ERROR: " + message);
|
System.out.println("ERROR: " + message);
|
||||||
|
if (throwable != null) {
|
||||||
throwable.printStackTrace();
|
throwable.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void e(String message) {
|
||||||
|
e(message, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.genymobile.scrcpy;
|
|||||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||||
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.media.MediaMuxer;
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
@@ -71,8 +70,9 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
codec.start();
|
codec.start();
|
||||||
try {
|
try {
|
||||||
alive = encode(codec, fd);
|
alive = encode(codec, fd);
|
||||||
} finally {
|
// do not call stop() on exception, it would trigger an IllegalStateException
|
||||||
codec.stop();
|
codec.stop();
|
||||||
|
} finally {
|
||||||
destroyDisplay(display);
|
destroyDisplay(display);
|
||||||
codec.release();
|
codec.release();
|
||||||
surface.release();
|
surface.release();
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.graphics.Rect;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public final class Server {
|
public final class Server {
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ public final class Server {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// synchronous
|
// synchronous
|
||||||
screenEncoder.streamScreen(device, connection.getFd());
|
screenEncoder.streamScreen(device, connection.getVideoFd());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// this is expected on close
|
// this is expected on close
|
||||||
Ln.d("Screen streaming stopped");
|
Ln.d("Screen streaming stopped");
|
||||||
@@ -49,8 +48,9 @@ public final class Server {
|
|||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
@SuppressWarnings("checkstyle:MagicNumber")
|
||||||
private static Options createOptions(String... args) {
|
private static Options createOptions(String... args) {
|
||||||
if (args.length != 5)
|
if (args.length != 5) {
|
||||||
throw new IllegalArgumentException("Expecting 5 parameters");
|
throw new IllegalArgumentException("Expecting 5 parameters");
|
||||||
|
}
|
||||||
|
|
||||||
Options options = new Options();
|
Options options = new Options();
|
||||||
|
|
||||||
@@ -73,6 +73,7 @@ public final class Server {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("checkstyle:MagicNumber")
|
||||||
private static Rect parseCrop(String crop) {
|
private static Rect parseCrop(String crop) {
|
||||||
if ("-".equals(crop)) {
|
if ("-".equals(crop)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
import android.view.InputEvent;
|
|
||||||
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -10,32 +10,42 @@ import java.lang.reflect.Method;
|
|||||||
public class StatusBarManager {
|
public class StatusBarManager {
|
||||||
|
|
||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private final Method expandNotificationsPanelMethod;
|
private Method expandNotificationsPanelMethod;
|
||||||
private final Method collapsePanelsMethod;
|
private Method collapsePanelsMethod;
|
||||||
|
|
||||||
public StatusBarManager(IInterface manager) {
|
public StatusBarManager(IInterface manager) {
|
||||||
this.manager = 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() {
|
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 {
|
try {
|
||||||
expandNotificationsPanelMethod.invoke(manager);
|
expandNotificationsPanelMethod.invoke(manager);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
throw new AssertionError(e);
|
Ln.e("Cannot invoke ServiceBarManager.expandNotificationsPanel()", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void collapsePanels() {
|
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 {
|
try {
|
||||||
collapsePanelsMethod.invoke(manager);
|
collapsePanelsMethod.invoke(manager);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
throw new AssertionError(e);
|
Ln.e("Cannot invoke ServiceBarManager.collapsePanels()", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user