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.

Java only, no Kotlin, no NDK. ADB stack vendored from
MuntashirAkon/libadb-android. scrcpy-server.jar is a pinned upstream binary
in `app/src/main/assets/`.


How to run it
-------------
Prerequisites: Android Studio (Iguana or newer) or JDK 17 + Android SDK with
platform-tools and platform 35.

  ./scripts/fetch-vendor          # pull libadb-android into vendor/
  ./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

For a signed release APK, see "How to build a release APK" below.

On the target device: Settings -> Developer options -> Wireless debugging ->
Pair device with pairing code. Note the displayed ip:port and 6-digit code.

In the app on the source device: enter ip:port and the pairing code, tap
Pair. Then tap the saved device row to connect and mirror.


How to test it
--------------
A `./test` entrypoint runs tests inside a project-local Docker image
(JDK 17, Android cmdline-tools, platform 35, build-tools 35, x86_64
emulator). The image is built on first run 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 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. If `apksigner` is reachable (either on $PATH or
under $ANDROID_SDK_ROOT/build-tools/) the script also runs a
post-build `apksigner verify`.

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 (attempt <n>)   (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.


Layout
------
  app/                    Android module (Java)
  vendor/libadb-android/  vendored libadb module, pinned tag, plus licenses
  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. Bundles libadb-android (dual GPL-3.0-or-later OR
Apache-2.0, used here under Apache-2.0) and a pinned, checksum-verified
scrcpy-server.jar from Genymobile/scrcpy (Apache-2.0).
