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. --- .gitignore | 18 + .gitmodules | 3 + LICENSE | 201 ++++++ README | 147 ++++ 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 +++ build.gradle | 4 + fastlane/metadata/android/en-US/changelogs/1.txt | 5 + .../metadata/android/en-US/full_description.txt | 20 + .../android/en-US/images/phoneScreenshots/1.png | Bin 0 -> 127122 bytes .../android/en-US/images/phoneScreenshots/2.png | Bin 0 -> 151143 bytes .../metadata/android/en-US/short_description.txt | 1 + fastlane/metadata/android/en-US/title.txt | 1 + gradle.properties | 3 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43504 bytes gradle/wrapper/gradle-wrapper.jar.sha256 | 1 + gradle/wrapper/gradle-wrapper.properties | 8 + gradlew | 249 +++++++ gradlew.bat | 92 +++ scripts/build-apk | 83 +++ scripts/check-wrapper | 24 + scripts/fetch-vendor | 44 ++ scripts/update-server | 64 ++ settings.gradle | 24 + test | 90 +++ test-rig/Dockerfile | 43 ++ test-rig/Keygen.java | 92 +++ test-rig/Pixhash.java | 54 ++ test-rig/build-apk.sh | 32 + test-rig/e2e.sh | 172 +++++ test-rig/record.sh | 178 +++++ test-rig/run.sh | 33 + test-rig/sdk-packages.txt | 5 + vendor/libadb-android/COPYING | 21 + vendor/libadb-android/LICENSES/Apache-2.0 | 186 +++++ vendor/libadb-android/LICENSES/BSD-3-Clause | 36 + vendor/libadb-android/LICENSES/GPL-3.0-or-later | 687 +++++++++++++++++++ vendor/libadb-android/LICENSES/MIT | 33 + vendor/libadb-android/README.md | 199 ++++++ vendor/libadb-android/SERVICES.md | 64 ++ vendor/libadb-android/libadb/.gitignore | 1 + vendor/libadb-android/libadb/build.gradle | 59 ++ .../libadb/src/main/AndroidManifest.xml | 4 + .../muntashirakon/adb/AbsAdbConnectionManager.java | 508 ++++++++++++++ .../adb/AdbAuthenticationFailedException.java | 14 + .../io/github/muntashirakon/adb/AdbConnection.java | 749 +++++++++++++++++++++ .../github/muntashirakon/adb/AdbInputStream.java | 43 ++ .../github/muntashirakon/adb/AdbOutputStream.java | 39 ++ .../adb/AdbPairingRequiredException.java | 7 + .../io/github/muntashirakon/adb/AdbProtocol.java | 497 ++++++++++++++ .../io/github/muntashirakon/adb/AdbStream.java | 300 +++++++++ .../io/github/muntashirakon/adb/AndroidPubkey.java | 244 +++++++ .../adb/ByteArrayNoThrowOutputStream.java | 24 + .../java/io/github/muntashirakon/adb/KeyPair.java | 38 ++ .../io/github/muntashirakon/adb/LocalServices.java | 271 ++++++++ .../io/github/muntashirakon/adb/PRNGFixes.java | 319 +++++++++ .../github/muntashirakon/adb/PairingAuthCtx.java | 131 ++++ .../muntashirakon/adb/PairingConnectionCtx.java | 384 +++++++++++ .../java/io/github/muntashirakon/adb/SslUtils.java | 124 ++++ .../io/github/muntashirakon/adb/StringCompat.java | 26 + .../github/muntashirakon/adb/android/AdbMdns.java | 193 ++++++ .../muntashirakon/adb/android/AndroidUtils.java | 58 ++ .../github/muntashirakon/adb/android/package.html | 1 + .../muntashirakon/adb/AndroidPubkeyTest.java | 118 ++++ vendor/scrcpy | 1 + 124 files changed, 12790 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 LICENSE create mode 100644 README 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 create mode 100644 build.gradle create mode 100644 fastlane/metadata/android/en-US/changelogs/1.txt create mode 100644 fastlane/metadata/android/en-US/full_description.txt create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/1.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/2.png create mode 100644 fastlane/metadata/android/en-US/short_description.txt create mode 100644 fastlane/metadata/android/en-US/title.txt create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.jar.sha256 create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100755 scripts/build-apk create mode 100755 scripts/check-wrapper create mode 100755 scripts/fetch-vendor create mode 100755 scripts/update-server create mode 100644 settings.gradle create mode 100755 test create mode 100644 test-rig/Dockerfile create mode 100644 test-rig/Keygen.java create mode 100644 test-rig/Pixhash.java create mode 100755 test-rig/build-apk.sh create mode 100755 test-rig/e2e.sh create mode 100755 test-rig/record.sh create mode 100755 test-rig/run.sh create mode 100644 test-rig/sdk-packages.txt create mode 100644 vendor/libadb-android/COPYING create mode 100644 vendor/libadb-android/LICENSES/Apache-2.0 create mode 100644 vendor/libadb-android/LICENSES/BSD-3-Clause create mode 100644 vendor/libadb-android/LICENSES/GPL-3.0-or-later create mode 100644 vendor/libadb-android/LICENSES/MIT create mode 100644 vendor/libadb-android/README.md create mode 100644 vendor/libadb-android/SERVICES.md create mode 100644 vendor/libadb-android/libadb/.gitignore create mode 100644 vendor/libadb-android/libadb/build.gradle create mode 100644 vendor/libadb-android/libadb/src/main/AndroidManifest.xml create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/AbsAdbConnectionManager.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/AdbAuthenticationFailedException.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/AdbInputStream.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/AdbOutputStream.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/AdbPairingRequiredException.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/AdbProtocol.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/AndroidPubkey.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/ByteArrayNoThrowOutputStream.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/KeyPair.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/LocalServices.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/PRNGFixes.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/PairingAuthCtx.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/PairingConnectionCtx.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/SslUtils.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/StringCompat.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/android/AdbMdns.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/android/AndroidUtils.java create mode 100644 vendor/libadb-android/libadb/src/main/java/io/github/muntashirakon/adb/android/package.html create mode 100644 vendor/libadb-android/libadb/src/test/java/io/github/muntashirakon/adb/AndroidPubkeyTest.java create mode 160000 vendor/scrcpy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6c7953 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.gradle/ +.tools/ +build/ +local.properties +*.iml +.idea/ +.DS_Store +app/release/ + +# vendored binary, regenerated by scripts/update-server +app/src/main/assets/scrcpy-server.jar + +# capture artefact from ./test record +output.mp4 + +# adb keypair lives in app's filesDir at runtime; never check it in +adbkey +adbkey.pub diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..580d485 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/scrcpy"] + path = vendor/scrcpy + url = https://github.com/Genymobile/scrcpy.git diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..288d3c7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative + Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, excluding + those notices that do not pertain to any part of the Derivative + Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and do + not modify the License. You may add Your own attribution notices + within Derivative Works that You distribute, alongside or as an + addendum to the NOTICE text from the Work, provided that such + additional attribution notices cannot be construed as modifying + the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions for + use, reproduction, or distribution of Your modifications, or for any + such Derivative Works as a whole, provided Your use, reproduction, + and distribution of the Work otherwise complies with the conditions + stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Lena + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README b/README new file mode 100644 index 0000000..a58e244 --- /dev/null +++ b/README @@ -0,0 +1,147 @@ +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= scid= + openAbstract scrcpy_ ok (attempt ) (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. + + +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). 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 @@ + + + + + + + + + + +