1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
|
scrcpy-android
==============
What it does
------------
Native Android app that mirrors a second Android device over Wireless ADB,
the same way scrcpy(1) does. Source device runs this app; target is any
Android 11+ device with Wireless Debugging enabled. Video, audio, touch,
key, and clipboard are forwarded. No root required on either side.
The application code is plain Java. Runtime dependencies include the native
Conscrypt and SPAKE2 libraries. The ADB stack is vendored from
MuntashirAkon/libadb-android. `scrcpy-server.jar` is a pinned upstream binary
in `app/src/main/assets/` and every APK packaging task verifies its checksum.
How to run it
-------------
Prerequisites: JDK 17 and Android SDK with platform-tools and platform 35, or
an Android Studio release supporting AGP 8.7.
./scripts/update-server # download + verify pinned scrcpy-server.jar
./gradlew :app:assembleDebug # build debug APK
adb install app/build/outputs/apk/debug/app-debug.apk
The vendored libadb-android tree is committed; scripts/fetch-vendor is
only needed when bumping its pinned tag, not for building.
F-Droid builds `scrcpy-server.jar` from the pinned source submodule. Its recipe
must pass the locally built artifact's checksum as
`-PscrcpyServerSha256=<sha256>`; normal builds verify the tracked upstream
binary checksum.
For a signed release APK, see "How to build a release APK" below.
On the target device: Settings -> Developer options -> Wireless debugging.
Note the "IP address & Port" on that screen; it is the device address and
the port is permanent. Then open "Pair device with pairing code" and note
the port from its dialog (a different, short-lived one) and the 6-digit
code.
In the app on the source device: enter the device address as `ip:port`,
then the pairing port and code, and tap Pair. Tap the saved device row to
connect and mirror.
How to test it
--------------
A direct host/VM quality gate is available when JDK 17 and Android SDK 35 are
installed:
./scripts/check # wrapper/server integrity, all JVM tests, Android lint
A `./test` entrypoint runs tests inside project-local Docker images. The small
unit target contains JDK 17, Android cmdline-tools, platform 35, and build-tools
35; emulator tiers add the x86_64 API-35 emulator. Images are built on first
use and cached.
./test # JVM unit tests (Wire, Sync, ControlMessages,
# Devices, plus piped-stream parser tests for
# VideoStream, AudioStream, ControlStream).
# ~30 s on warm cache. No KVM needed.
./test e2e # Single-emulator self-mirror end-to-end.
# Boots the API-35 AOSP emulator, roots it,
# installs the debug APK, drops the app's public
# key into /data/misc/adb/adb_keys, launches
# Mirror against 127.0.0.1:5555, then asserts
# 'video frame n=1' lands in logcat and the
# screencap is non-uniform. Then kills the server
# on the target and asserts the session detects the
# dropped link and auto-reconnects (a fresh
# 'video frame n=1'). Needs /dev/kvm readable by the
# invoking uid.
./test all # unit tests followed by e2e. Fails fast: if the
# unit tier fails, the e2e tier is skipped.
# Same /dev/kvm requirement as `./test e2e`.
./test record # Boots the emulator, runs a Mirror session, injects
# taps/keys, and stitches a screen recording into
# ./output.mp4. Needs /dev/kvm.
./test screenshots
# Boots a pixel_6 AVD (natively 1080x2400), captures
# Main and Settings, and overwrites the tracked PNGs
# under fastlane/metadata/android/en-US/images/
# phoneScreenshots. Refuses to write a shot unless the
# app owns the focused window and the PNG has the
# expected geometry. Review the diff before
# committing. Needs /dev/kvm.
./test apk # Smoke build: generates a throwaway PKCS12 keystore
# under .tools/, runs scripts/build-apk against it,
# verifies the resulting APK is signed. No KVM.
If /dev/kvm is not rw for your uid the script prints the one-line fix
and exits - it does not auto-chmod.
How to build a release APK
--------------------------
Set four environment variables and run the release script:
export KEYSTORE_PATH=/abs/path/to/release.jks
export KEYSTORE_PASS=...
export KEY_ALIAS=...
export KEY_PASS=...
./scripts/build-apk
The signed APK lands at
`app/build/outputs/apk/release/app-release.apk` and its sha256 is
printed at the end. Build-tools 35.0.0 `apksigner` must be available on
`$PATH` or under `$ANDROID_SDK_ROOT`; the script fails unless independent
signature verification succeeds.
Generate a release keystore once with:
keytool -genkey -v -keystore release.jks -storetype PKCS12 \
-alias scrcpy-android -keyalg RSA -keysize 2048 -validity 10000
Keep it OUTSIDE the repo. The .gitignore does not protect arbitrary
filenames; the keystore is your secret.
How to debug it
---------------
Everything is logged with tag `scrcpy-android`:
adb logcat -s scrcpy-android
Expected log sequence on a successful connect (pair ok appears once,
at pairing time):
pair ok host=... pair_port=...
adb connect ok
push /data/local/tmp/scrcpy-server.jar bytes=...
spawn server ver=<v> scid=<hex>
openAbstract scrcpy_<scid> ok (x3)
video meta codec=h264
audio meta codec=raw (or opus if selected in Settings)
video session meta <w>x<h> client_resize=false
video frame n=1
Server stdout/stderr is pumped into the same log stream prefixed with
`server: ` so target-side errors are visible without an extra adb shell.
Security and privacy
--------------------
Pairing creates a software-backed ADB key under the app's private files
directory. Revoke it from the target's Wireless debugging screen or clear the
app's data. Clipboard synchronization is automatic in both directions while
a session is connected; do not mirror an untrusted target while sensitive
clipboard contents are present.
Recordings and crash logs are written under the app-specific external files
directory and are removed when the app is uninstalled. They may contain
screen contents, entered text, addresses, and diagnostic data.
The continuous foreground service is classified as `mediaPlayback` because
it presents a live remote audio/video stream. This avoids Android 15's
six-hour `dataSync` foreground-service timeout and adds no runtime permission
prompt.
Layout
------
app/ Android module (Java)
vendor/libadb-android/ vendored libadb module, pinned tag, plus licenses
vendor/scrcpy git submodule pinned at the scrcpy release the
server jar comes from; F-Droid builds the jar
from it. Not needed for local builds.
fastlane/ F-Droid store metadata (description, icon,
screenshots)
scripts/ POSIX sh, set -eu
gradle/, gradlew* Gradle 8.9 wrapper
LICENSE Apache-2.0
Constraints
-----------
- minSdk 31 (source device needs Android 12+); compileSdk = targetSdk =
35 (built against and targeting Android 15)
- Wireless ADB only; no USB-OTG host mode
- Multi-touch, soft keys, clipboard sync, and hardware Back (long-press
the device Back key while Mirror is foreground) are forwarded.
- Video codec (h264 / h265 / av1) and audio codec (raw / opus) are
selectable from the in-app Settings screen; defaults are h264 + raw.
License
-------
Apache-2.0; see LICENSE. Bundled dependency licenses, source locations, and
the LGPL replacement notice for SPAKE2 are recorded in
`app/src/main/assets/THIRD_PARTY_NOTICES`, which is shipped in the APK.
|