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.
