From eb0c8951196c637e44daf3c0617b131b997d5d2c Mon Sep 17 00:00:00 2001 From: Lena Date: Thu, 1 Jan 2026 00:00:00 +0000 Subject: scrcpy-android: mirror an Android device over wireless ADB Native Java app for Android 12+ that mirrors another Android device over wireless ADB, forwarding video, audio, touch input, and clipboard. Bundles a pinned scrcpy-server.jar and the vendored libadb-android stack. Supports h264/h265/av1 video and raw/opus audio with in-app codec selection. No NDK, no Kotlin. Includes JVM unit tests and a Docker-based emulator e2e rig. --- app/build.gradle | 130 ++++++ app/proguard-rules.pro | 41 ++ app/src/debug/res/layout/mirror.xml | 93 ++++ app/src/main/AndroidManifest.xml | 49 +++ app/src/main/assets/scrcpy-server.sha256 | 1 + app/src/main/assets/scrcpy-server.version | 1 + app/src/main/java/invalid/lena/scrcpy/Adb.java | 210 +++++++++ app/src/main/java/invalid/lena/scrcpy/App.java | 15 + .../main/java/invalid/lena/scrcpy/AtomicFiles.java | 40 ++ .../main/java/invalid/lena/scrcpy/AudioFrames.java | 18 + .../main/java/invalid/lena/scrcpy/AudioSink.java | 215 ++++++++++ .../main/java/invalid/lena/scrcpy/AudioStream.java | 93 ++++ .../java/invalid/lena/scrcpy/ControlMessages.java | 72 ++++ .../java/invalid/lena/scrcpy/ControlStream.java | 154 +++++++ .../main/java/invalid/lena/scrcpy/Controller.java | 173 ++++++++ .../main/java/invalid/lena/scrcpy/Crashlog.java | 48 +++ app/src/main/java/invalid/lena/scrcpy/Devices.java | 126 ++++++ app/src/main/java/invalid/lena/scrcpy/Log.java | 26 ++ app/src/main/java/invalid/lena/scrcpy/Main.java | 150 +++++++ app/src/main/java/invalid/lena/scrcpy/Mirror.java | 471 +++++++++++++++++++++ .../main/java/invalid/lena/scrcpy/MuxRecorder.java | 225 ++++++++++ app/src/main/java/invalid/lena/scrcpy/Server.java | 304 +++++++++++++ app/src/main/java/invalid/lena/scrcpy/Session.java | 318 ++++++++++++++ .../main/java/invalid/lena/scrcpy/Sessions.java | 75 ++++ .../main/java/invalid/lena/scrcpy/Settings.java | 93 ++++ .../java/invalid/lena/scrcpy/SettingsActivity.java | 110 +++++ app/src/main/java/invalid/lena/scrcpy/Sync.java | 78 ++++ .../main/java/invalid/lena/scrcpy/VideoFrames.java | 17 + .../java/invalid/lena/scrcpy/VideoRecorder.java | 19 + .../main/java/invalid/lena/scrcpy/VideoSink.java | 244 +++++++++++ .../main/java/invalid/lena/scrcpy/VideoStream.java | 156 +++++++ app/src/main/java/invalid/lena/scrcpy/Wire.java | 99 +++++ app/src/main/res/drawable/bg_card.xml | 8 + app/src/main/res/drawable/bg_field.xml | 18 + app/src/main/res/drawable/bg_status_pill.xml | 7 + app/src/main/res/drawable/btn_primary.xml | 11 + app/src/main/res/drawable/btn_secondary.xml | 12 + app/src/main/res/drawable/ic_device.xml | 14 + .../main/res/drawable/ic_launcher_background.xml | 7 + .../main/res/drawable/ic_launcher_foreground.xml | 52 +++ app/src/main/res/drawable/ic_notification.xml | 21 + app/src/main/res/drawable/ic_settings.xml | 12 + app/src/main/res/layout/device_row.xml | 28 ++ app/src/main/res/layout/main.xml | 181 ++++++++ app/src/main/res/layout/mirror.xml | 67 +++ app/src/main/res/layout/settings.xml | 294 +++++++++++++ app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 6 + app/src/main/res/values/colors.xml | 16 + app/src/main/res/values/dimens.xml | 17 + app/src/main/res/values/ids.xml | 10 + app/src/main/res/values/strings.xml | 36 ++ app/src/main/res/values/styles.xml | 73 ++++ app/src/main/res/values/themes.xml | 32 ++ .../java/invalid/lena/scrcpy/AtomicFilesTest.java | 54 +++ .../java/invalid/lena/scrcpy/AudioStreamTest.java | 123 ++++++ .../invalid/lena/scrcpy/ControlMessagesTest.java | 65 +++ .../invalid/lena/scrcpy/ControlStreamTest.java | 152 +++++++ .../test/java/invalid/lena/scrcpy/DevicesTest.java | 64 +++ .../test/java/invalid/lena/scrcpy/SyncTest.java | 137 ++++++ .../java/invalid/lena/scrcpy/VideoStreamTest.java | 245 +++++++++++ .../test/java/invalid/lena/scrcpy/WireTest.java | 94 ++++ 61 files changed, 5720 insertions(+) create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/debug/res/layout/mirror.xml create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/scrcpy-server.sha256 create mode 100644 app/src/main/assets/scrcpy-server.version create mode 100644 app/src/main/java/invalid/lena/scrcpy/Adb.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/App.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/AtomicFiles.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/AudioFrames.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/AudioSink.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/AudioStream.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/ControlMessages.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/ControlStream.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Controller.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Crashlog.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Devices.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Log.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Main.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Mirror.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/MuxRecorder.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Server.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Session.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Sessions.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Settings.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/SettingsActivity.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Sync.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/VideoFrames.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/VideoRecorder.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/VideoSink.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/VideoStream.java create mode 100644 app/src/main/java/invalid/lena/scrcpy/Wire.java create mode 100644 app/src/main/res/drawable/bg_card.xml create mode 100644 app/src/main/res/drawable/bg_field.xml create mode 100644 app/src/main/res/drawable/bg_status_pill.xml create mode 100644 app/src/main/res/drawable/btn_primary.xml create mode 100644 app/src/main/res/drawable/btn_secondary.xml create mode 100644 app/src/main/res/drawable/ic_device.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_notification.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/layout/device_row.xml create mode 100644 app/src/main/res/layout/main.xml create mode 100644 app/src/main/res/layout/mirror.xml create mode 100644 app/src/main/res/layout/settings.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/ids.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/test/java/invalid/lena/scrcpy/AtomicFilesTest.java create mode 100644 app/src/test/java/invalid/lena/scrcpy/AudioStreamTest.java create mode 100644 app/src/test/java/invalid/lena/scrcpy/ControlMessagesTest.java create mode 100644 app/src/test/java/invalid/lena/scrcpy/ControlStreamTest.java create mode 100644 app/src/test/java/invalid/lena/scrcpy/DevicesTest.java create mode 100644 app/src/test/java/invalid/lena/scrcpy/SyncTest.java create mode 100644 app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java create mode 100644 app/src/test/java/invalid/lena/scrcpy/WireTest.java (limited to 'app') diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..d16a8d5 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,130 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'invalid.lena.scrcpy' + compileSdk 35 + // Pin build-tools explicitly so the aapt2/zipalign/d8 toolchain is + // fixed and builds stay reproducible, not whatever AGP defaults to. + // 35.0.0 is the compileSdk's. + buildToolsVersion '35.0.0' + + defaultConfig { + // The published package id. Immutable across releases. + applicationId 'invalid.lena.scrcpy' + minSdk 31 + targetSdk 35 + versionCode 1 + versionName '0.1' + } + + // AGP otherwise embeds a Google-signed dependency-metadata blob in the + // APK that is not byte-reproducible. Strip it so the build stays + // deterministic. This is the single most common reproducible-build + // breaker on AGP. + dependenciesInfo { + includeInApk false + includeInBundle false + } + + // Release signing pulls credentials from the environment. Set all + // four of KEYSTORE_PATH / KEYSTORE_PASS / KEY_ALIAS / KEY_PASS and + // assembleRelease produces a signed APK. With any of them missing + // the release build is unsigned (Gradle's default). + signingConfigs { + release { + def ksPath = System.getenv('KEYSTORE_PATH') + def ksPass = System.getenv('KEYSTORE_PASS') + def kAlias = System.getenv('KEY_ALIAS') + def kPass = System.getenv('KEY_PASS') + if (ksPath && ksPass && kAlias && kPass) { + storeFile file(ksPath) + storePassword ksPass + keyAlias kAlias + keyPassword kPass + } + } + } + + // Native ABI policy: release ships arm64-v8a only. Every targeted + // source device is 64-bit ARM in practice; 32-bit-only devices are + // not supported. The test rig uses an x86_64 emulator, so debug keeps + // x86_64 (and arm64) so ./test e2e still works. + buildTypes { + debug { + applicationIdSuffix '.debug' + versionNameSuffix '-debug' + ndk { + abiFilters 'arm64-v8a', 'x86_64' + } + } + release { + ndk { + abiFilters 'arm64-v8a' + } + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), + 'proguard-rules.pro' + if (System.getenv('KEYSTORE_PATH')) { + signingConfig signingConfigs.release + } + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + packaging { + // bouncycastle (transitive via :adb) is the dominant source of + // dead weight in the APK. Each entry below is dead code/data for + // our use case (we only call BC's asn1, crypto, util.encoders): + // - picnic post-quantum lookup tables: ~1.2 MB of .properties + // - cert-path-reviewer i18n messages: ~92 KB, en+de only + // - kotlin metadata: bleed from spake2-android (Kotlin lib); + // our code is pure Java and doesn't reflect on .kotlin_builtins + // - androidx annotation LICENSE.txt: 10 KB blob, no runtime use + // - duplicate META-INF licenses/notices: ordinary AGP cleanup + resources.excludes += [ + 'META-INF/LICENSE*', + 'META-INF/NOTICE*', + 'META-INF/versions/9/OSGI-INF/MANIFEST.MF', + 'META-INF/androidx/**', + 'META-INF/kotlin-stdlib*', + 'org/bouncycastle/pqc/crypto/picnic/**', + 'org/bouncycastle/x509/CertPathReviewerMessages*.properties', + 'kotlin/**', + ] + } + + testOptions { + // android.util.Log and friends are stubs on the JVM unit-test + // classpath; let them return defaults instead of throwing. + unitTests.returnDefaultValues = true + } +} + +dependencies { + implementation project(':adb') + // bcprov is already pulled in transitively by :adb at runtime; we + // need it on the compile classpath too for Adb.java's ASN.1 + // cert-builder. :adb keeps it 'implementation'-scoped upstream so + // we declare it explicitly here rather than patch the vendor tree. + implementation 'org.bouncycastle:bcprov-jdk15to18:1.81' + + // Bundled Conscrypt. libadb-android's TLS pairing exports keying + // material via Conscrypt. Without a bundled copy it reflects into the + // platform's hidden com.android.org.conscrypt.Conscrypt, which under + // targetSdk 35 is invisible to reflection (getMethod throws + // NoSuchMethodException and pairing fails on real devices). Bundling a + // standalone Conscrypt flips libadb onto the public + // org.conscrypt.Conscrypt API, which is not restricted. Ships a native + // .so (filtered to arm64-v8a in release). + implementation 'org.conscrypt:conscrypt-android:2.5.2' + + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.json:json:20240303' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..a2c74bf --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,41 @@ +# R8 keep rules. The goal is to let R8 strip everything the project +# doesn't use while keeping the runtime paths the vendored libraries +# rely on. We use bouncycastle directly (asn1/crypto/util.encoders) +# and via :adb (pairing crypto), so its dead weight - pqc, jcajce, +# pkix, cert, cms, tls, openpgp - is fair game for R8. Packaging +# excludes drop the matching resources alongside. + +# ---- bouncycastle ---- +# asn1 has heavy static OID-to-class lookup; keep the full surface. +-keep class org.bouncycastle.asn1.** { *; } +# crypto primitives we call directly + via :adb (SHA256Digest, +# AESEngine, GCMBlockCipher, HKDF, AEADParameters, KeyParameter). +-keep class org.bouncycastle.crypto.** { *; } +# util.encoders.Base64; rest of util/* is also referenced from asn1. +-keep class org.bouncycastle.util.** { *; } +-dontwarn org.bouncycastle.** + +# ---- libadb-android (vendored as :adb) ---- +# AbsAdbConnectionManager + AdbStream + LocalServices are the public +# surface we touch. Pairing internals reach into Conscrypt/Provider +# reflectively; keep the package wholesale, it's ~150 KB. +-keep class io.github.muntashirakon.adb.** { *; } +-dontwarn io.github.muntashirakon.adb.** + +# Conscrypt is now bundled (org.conscrypt:conscrypt-android) so TLS +# pairing uses its public exportKeyingMaterial instead of the platform's +# hidden one. libadb reaches it reflectively (Class.forName + +# getDeclaredConstructor + getMethod), and Conscrypt self-references via +# JNI, so keep the package wholesale. The platform conscrypt name still +# appears in a dead else-branch, so silence that one. +-keep class org.conscrypt.** { *; } +-dontwarn org.conscrypt.** +-dontwarn com.android.org.conscrypt.** +# Legacy Apache Harmony JSSE; unreachable on minSdk 35 but +# referenced in libadb's PRNGFixes. +-dontwarn org.apache.harmony.** + +# spake2-android (Kotlin) - small native bridge for SPAKE2 pairing. +# Native methods are looked up by JNI signature; keep the names. +-keep class com.muntashirakon.crypto.spake2.** { *; } +-dontwarn com.muntashirakon.crypto.spake2.** diff --git a/app/src/debug/res/layout/mirror.xml b/app/src/debug/res/layout/mirror.xml new file mode 100644 index 0000000..5d0e845 --- /dev/null +++ b/app/src/debug/res/layout/mirror.xml @@ -0,0 +1,93 @@ + + + + + + + + + + +