From e9315ab503443a4e05ec9c9b363e73a174ef82d6 Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 1 Aug 2026 00:00:00 +0000 Subject: docs: document verified release behavior --- README | 335 ++++++++++++++++++++++++++++++----------------------------------- 1 file changed, 153 insertions(+), 182 deletions(-) (limited to 'README') diff --git a/README b/README index 11de229..707f1f6 100644 --- a/README +++ b/README @@ -1,197 +1,168 @@ scrcpy-android ============== +Native Android client that mirrors and controls another Android device over +Wireless debugging. It forwards video, audio, touch, keys, and clipboard data +through the scrcpy server. Neither device needs root. +The source device running this app needs Android 12 or newer. The target needs +Android 11 or newer with Wireless debugging enabled. -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. +Run +--- +Build requirements are JDK 17, Android SDK platform 36, and build-tools 35. + ./scripts/update-server + ./gradlew :app:assembleDebug + adb install app/build/outputs/apk/debug/app-debug.apk -How to run it +On the target, open Settings, Developer options, Wireless debugging. Copy the +displayed IP address and connection port. Open Pair device with pairing code +and copy its separate pairing port and six-digit code. + +Enter those values in the source app and tap Pair and save. Pairing authorizes +this app's stable ADB key on the target. + +Android may change the Wireless debugging port after Wireless debugging is +toggled, adbd restarts, or the device reboots. Forget the stale row and pair +again with the newly displayed address. + + +Test +---- +The host gate checks the Gradle wrapper and server inputs, runs both JVM test +suites, and runs Android lint: + + ./scripts/check + +The Docker entrypoint supplies its own JDK and Android SDK: + + ./test unit + ./test server + ./test apk + ./test e2e + ./test all + +`unit` and `server` need Docker only. `server` builds and tests the pinned +scrcpy source, then compares its executable dex with the bundled release. +`apk` signs and verifies a minified release with a throwaway key. `e2e` needs +read/write access to `/dev/kvm`; it boots two clean API 36 emulators and +exercises Android's real pairing-code UI, the minified +release client, mandatory TLS, Opus playback, SurfaceView rendering, and +touch, key, and clipboard forwarding. It also checks automatic reconnect +across both scrcpy and adbd restarts. + +`e2e` boots two emulators and drives real system UI, so it is slower and more +timing-dependent than the rest of the gate. Every deadline is overridable if a +host needs longer: `E2E_BOOT_DEADLINE`, `E2E_SETTLE_DEADLINE`, +`E2E_UI_DEADLINE`, `E2E_ROTATE_DEADLINE`, `E2E_RESIZE_DEADLINE`. +The disposable AVDs use a bounded 10 GiB tmpfs; allow roughly 16 GiB of +available memory for the two emulators and their data. + +The screenshot tool also needs `/dev/kvm`: + + ./test screenshots + +It replaces the tracked Fastlane screenshots only after checking their owner +and dimensions. + + +Release +------- +Set all four signing variables and run the release script: + + export KEYSTORE_PATH=/absolute/path/release.p12 + export KEYSTORE_PASS=... + export KEY_ALIAS=... + export KEY_PASS=... + ./scripts/build-apk + +The script verifies the wrapper and scrcpy server, builds the minified APK, +checks its signature, 16 KiB page alignment, SDK levels, exact permission set, +and exact ABI set with build-tools 35, then prints its SHA-256. The APK contains +`arm64-v8a` and `x86_64` native libraries. + +Keep the signing keystore outside this repository. `.gitignore` cannot protect +an arbitrary keystore filename. + + +Debug +----- +All application logs use one tag: + + adb logcat -s scrcpy-android + +A normal session includes these lines: + + pair ok host=... pair_port=... + adb connect ok + push /data/local/tmp/scrcpy-server.jar bytes=... + spawn server ver=... scid=... + openAbstract scrcpy_... ok + video meta codec=h264 + audio meta codec=opus + video sink: rendered frame n=1 + audio sink: playback started + +Target-side scrcpy output is copied into the same log with the `server:` +prefix. Uncaught exceptions are also written under the app-specific external +files directory as `crash-*.log`; only the five newest files are retained. + + +Security +-------- +The app creates one software-backed RSA ADB identity in its private files +directory. Pairing authenticates the six-digit-code exchange with SPAKE2 and +authorizes that client key on the target. Later connections require TLS and +the target rejects clients whose keys were not authorized. + +Android does not provide a stable target certificate to pin: its pairing +server generates a fresh key and adbd uses a separate process-scoped key. +Consequently the ADB protocol authenticates this client to the target, but +does not cryptographically authenticate a later target to this client. TLS +still protects against passive eavesdropping. Treat the saved address and the +network carrying it as trusted. + +Clipboard synchronization is enabled by default and works in both directions. +Disable it in Settings before connecting to a target that should not receive +the source device's clipboard. Only explicit text clipboard items are synced. +On Android 10 and newer, a source-side copy made in another app is forwarded +when this app regains focus, because Android blocks background clipboard reads. + +Use a private network or VPN to reach a remote target. Do not expose a Wireless +debugging port directly to the internet. The app performs no hidden network +access; it connects only to the address selected by the user and downloads +nothing at runtime. + +Backups and device-to-device transfer are disabled because private app data +contains the ADB identity and saved target addresses. Crash logs may contain +addresses and diagnostic data and are removed when the app is uninstalled. + + +Source layout ------------- -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 +`app/` contains the Android application. `vendor/libadb-android/` is a pruned, +locally patched subset of libadb-android 3.1.1. `vendor/scrcpy` is a submodule +pinned to the scrcpy release used for the server. `test-rig/` contains the +Docker and emulator tests. -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=`; normal builds verify the tracked upstream -binary checksum. +The server binary is intentionally ignored by Git. `scripts/update-server` +downloads the allow-listed scrcpy 4.1 artifact and verifies its SHA-256 before +installing it. Every APK build verifies it again. The source-build equivalent +used by F-Droid is explicit: -For a signed release APK, see "How to build a release APK" below. + server=$(./scripts/build-server) + install -m 0644 "$server" app/src/main/assets/scrcpy-server.jar + sha256sum app/src/main/assets/scrcpy-server.jar \ + | awk '{ print $1 }' >app/src/main/assets/scrcpy-server.sha256 + ./gradlew :app:assembleRelease -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= scid= - openAbstract scrcpy_ ok (x3) - video meta codec=h264 - audio meta codec=raw (or opus if selected in Settings) - video session meta x 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. +Vendor updates are manual because the ADB transport carries overlapping local +security and flow-control changes. See `vendor/libadb-android/README.md`. 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. +The application is Apache-2.0; see `LICENSE`. Bundled component notices, +source locations, and LGPL relinking information are in +`app/src/main/assets/THIRD_PARTY_NOTICES` and in the in-app Licenses screen. -- cgit v1.2.3