From 005d65330de45640c64ca38c7441bdf407a71a12 Mon Sep 17 00:00:00 2001
From: liuguijing <123456>
Date: Mon, 10 Jun 2024 22:10:55 +0800
Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AC=A1=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 15 +
app/.gitignore | 1 +
app/build.gradle | 107 +++
app/proguard-rules.pro | 21 +
.../wpywapp/ExampleInstrumentedTest.java | 26 +
app/src/main/AndroidManifest.xml | 51 +
.../java/com/xypower/wpywapp/ADBManager.java | 20 +
.../com/xypower/wpywapp/MainActivity.java | 398 ++++++++
.../com/xypower/wpywapp/WpywApplication.java | 47 +
.../xypower/wpywapp/base/BaseActivity.java | 82 ++
.../xypower/wpywapp/base/BaseFragment.java | 85 ++
.../xypower/wpywapp/base/BaseViewModel.java | 10 +
.../xypower/wpywapp/bean/TerminalBean.java | 32 +
.../wpywapp/interfaces/MainActCallback.java | 15 +
.../wpywapp/jadb/AdbFilterInputStream.java | 47 +
.../wpywapp/jadb/AdbFilterOutputStream.java | 21 +
.../wpywapp/jadb/AdbServerLauncher.java | 48 +
.../ConnectionToRemoteDeviceException.java | 7 +
.../wpywapp/jadb/DeviceDetectionListener.java | 9 +
.../xypower/wpywapp/jadb/DeviceWatcher.java | 44 +
.../jadb/HostConnectToRemoteTcpDevice.java | 29 +
.../wpywapp/jadb/HostConnectionCommand.java | 70 ++
.../HostDisconnectFromRemoteTcpDevice.java | 30 +
.../wpywapp/jadb/ITransportFactory.java | 10 +
.../xypower/wpywapp/jadb/JadbConnection.java | 83 ++
.../com/xypower/wpywapp/jadb/JadbDevice.java | 256 +++++
.../xypower/wpywapp/jadb/JadbException.java | 10 +
.../jadb/LookBackFilteringOutputStream.java | 45 +
.../com/xypower/wpywapp/jadb/RemoteFile.java | 31 +
.../wpywapp/jadb/RemoteFileRecord.java | 34 +
.../java/com/xypower/wpywapp/jadb/Stream.java | 27 +
.../com/xypower/wpywapp/jadb/Subprocess.java | 9 +
.../xypower/wpywapp/jadb/SyncTransport.java | 115 +++
.../com/xypower/wpywapp/jadb/Transport.java | 75 ++
.../xypower/wpywapp/jadb/managers/Bash.java | 11 +
.../wpywapp/jadb/managers/Package.java | 26 +
.../wpywapp/jadb/managers/PackageManager.java | 144 +++
.../jadb/managers/PropertyManager.java | 57 ++
.../jadb/server/AdbDeviceResponder.java | 25 +
.../jadb/server/AdbProtocolHandler.java | 248 +++++
.../wpywapp/jadb/server/AdbResponder.java | 14 +
.../wpywapp/jadb/server/AdbServer.java | 26 +
.../wpywapp/jadb/server/SocketServer.java | 71 ++
.../com/xypower/wpywapp/libadb/.gitignore | 1 +
.../com/xypower/wpywapp/libadb/build.gradle | 60 ++
.../libadb/src/main/AndroidManifest.xml | 4 +
.../adb/AbsAdbConnectionManager.java | 509 ++++++++++
.../adb/AdbAuthenticationFailedException.java | 14 +
.../muntashirakon/adb/AdbConnection.java | 749 +++++++++++++++
.../adb/AdbConnectionManager.java | 170 ++++
.../muntashirakon/adb/AdbInputStream.java | 43 +
.../muntashirakon/adb/AdbOutputStream.java | 39 +
.../adb/AdbPairingRequiredException.java | 7 +
.../github/muntashirakon/adb/AdbProtocol.java | 497 ++++++++++
.../github/muntashirakon/adb/AdbStream.java | 300 ++++++
.../muntashirakon/adb/AndroidPubkey.java | 245 +++++
.../adb/ByteArrayNoThrowOutputStream.java | 24 +
.../io/github/muntashirakon/adb/KeyPair.java | 38 +
.../muntashirakon/adb/LocalServices.java | 271 ++++++
.../github/muntashirakon/adb/PRNGFixes.java | 319 ++++++
.../muntashirakon/adb/PairingAuthCtx.java | 132 +++
.../adb/PairingConnectionCtx.java | 384 ++++++++
.../io/github/muntashirakon/adb/SslUtils.java | 124 +++
.../muntashirakon/adb/StringCompat.java | 26 +
.../muntashirakon/adb/android/AdbMdns.java | 189 ++++
.../adb/android/AndroidUtils.java | 58 ++
.../muntashirakon/adb/android/package.html | 1 +
.../muntashirakon/adb/AndroidPubkeyTest.java | 118 +++
.../com/xypower/wpywapp/page/BuildConfig.java | 10 +
.../wpywapp/page/DataBindingActivity.java | 70 ++
.../wpywapp/page/DataBindingConfig.java | 49 +
.../wpywapp/page/DataBindingFragment.java | 91 ++
.../xypower/wpywapp/tool/CommandResult.java | 47 +
.../xypower/wpywapp/tool/LocationUtil.java | 282 ++++++
.../com/xypower/wpywapp/tool/RegexUtil.java | 152 +++
.../com/xypower/wpywapp/tool/ShellUtils.java | 204 ++++
.../java/com/xypower/wpywapp/tool/Utils.java | 337 +++++++
.../com/xypower/wpywapp/tool/testCmd.java | 149 +++
.../xypower/wpywapp/ui/BottomActivity.java | 57 ++
.../ui/dashboard/DashboardFragment.java | 43 +
.../ui/dashboard/DashboardViewModel.java | 11 +
.../xypower/wpywapp/ui/home/HomeFragment.java | 41 +
.../wpywapp/ui/home/HomeViewModel.java | 19 +
.../notifications/NotificationsFragment.java | 40 +
.../notifications/NotificationsViewModel.java | 19 +
.../wpywapp/viewmoel/BottomViewModel.java | 19 +
.../res/drawable/ic_dashboard_black_24dp.xml | 9 +
.../main/res/drawable/ic_home_black_24dp.xml | 9 +
.../res/drawable/ic_launcher_background.xml | 170 ++++
.../res/drawable/ic_launcher_foreground.xml | 30 +
.../drawable/ic_notifications_black_24dp.xml | 9 +
app/src/main/res/layout/activity_bottom.xml | 42 +
app/src/main/res/layout/activity_main.xml | 51 +
.../main/res/layout/fragment_dashboard.xml | 83 ++
app/src/main/res/layout/fragment_home.xml | 906 ++++++++++++++++++
.../res/layout/fragment_notifications.xml | 83 ++
app/src/main/res/menu/bottom_nav_menu.xml | 19 +
.../main/res/mipmap-anydpi/ic_launcher.xml | 6 +
.../res/mipmap-anydpi/ic_launcher_round.xml | 6 +
app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes
.../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes
app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes
.../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes
.../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes
.../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes
.../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes
.../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes
.../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes
.../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes
app/src/main/res/mipmap-xxxhdpi/search.png | Bin 0 -> 9160 bytes
app/src/main/res/mipmap-xxxhdpi/takepic.png | Bin 0 -> 5631 bytes
.../main/res/navigation/mobile_navigation.xml | 25 +
app/src/main/res/values-night/themes.xml | 7 +
app/src/main/res/values/colors.xml | 5 +
app/src/main/res/values/dimens.xml | 5 +
app/src/main/res/values/strings.xml | 7 +
app/src/main/res/values/themes.xml | 9 +
app/src/main/res/xml/backup_rules.xml | 13 +
.../main/res/xml/data_extraction_rules.xml | 19 +
.../main/res/xml/network_security_config.xml | 6 +
.../com/xypower/wpywapp/ExampleUnitTest.java | 51 +
.../java/com/xypower/wpywapp/StringUtils.java | 361 +++++++
build.gradle | 4 +
gradle.properties | 23 +
gradlew | 185 ++++
gradlew.bat | 89 ++
settings.gradle | 25 +
sign/platform.jks | Bin 0 -> 2364 bytes
128 files changed, 10751 insertions(+)
create mode 100644 .gitignore
create mode 100644 app/.gitignore
create mode 100644 app/build.gradle
create mode 100644 app/proguard-rules.pro
create mode 100644 app/src/androidTest/java/com/xypower/wpywapp/ExampleInstrumentedTest.java
create mode 100644 app/src/main/AndroidManifest.xml
create mode 100644 app/src/main/java/com/xypower/wpywapp/ADBManager.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/MainActivity.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/WpywApplication.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/base/BaseActivity.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/base/BaseFragment.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/base/BaseViewModel.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/bean/TerminalBean.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/interfaces/MainActCallback.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterInputStream.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterOutputStream.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/AdbServerLauncher.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/ConnectionToRemoteDeviceException.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/DeviceDetectionListener.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/DeviceWatcher.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/HostConnectToRemoteTcpDevice.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/HostConnectionCommand.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/HostDisconnectFromRemoteTcpDevice.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/ITransportFactory.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/JadbConnection.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/JadbDevice.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/JadbException.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/LookBackFilteringOutputStream.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/RemoteFile.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/RemoteFileRecord.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/Stream.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/Subprocess.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/SyncTransport.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/Transport.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/managers/Bash.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/managers/Package.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/managers/PackageManager.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/managers/PropertyManager.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/server/AdbDeviceResponder.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/server/AdbProtocolHandler.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/server/AdbResponder.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/server/AdbServer.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/jadb/server/SocketServer.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/.gitignore
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/build.gradle
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/AndroidManifest.xml
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AbsAdbConnectionManager.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbAuthenticationFailedException.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnectionManager.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbInputStream.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbOutputStream.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbPairingRequiredException.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbProtocol.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AndroidPubkey.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/ByteArrayNoThrowOutputStream.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/KeyPair.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/LocalServices.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PRNGFixes.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingAuthCtx.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingConnectionCtx.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/SslUtils.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/StringCompat.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AdbMdns.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AndroidUtils.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/package.html
create mode 100644 app/src/main/java/com/xypower/wpywapp/libadb/src/test/java/io/github/muntashirakon/adb/AndroidPubkeyTest.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/page/BuildConfig.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/page/DataBindingActivity.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/page/DataBindingConfig.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/page/DataBindingFragment.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/tool/CommandResult.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/tool/LocationUtil.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/tool/RegexUtil.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/tool/ShellUtils.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/tool/Utils.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/tool/testCmd.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/ui/BottomActivity.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/ui/dashboard/DashboardFragment.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/ui/dashboard/DashboardViewModel.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/ui/home/HomeFragment.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/ui/home/HomeViewModel.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/ui/notifications/NotificationsFragment.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/ui/notifications/NotificationsViewModel.java
create mode 100644 app/src/main/java/com/xypower/wpywapp/viewmoel/BottomViewModel.java
create mode 100644 app/src/main/res/drawable/ic_dashboard_black_24dp.xml
create mode 100644 app/src/main/res/drawable/ic_home_black_24dp.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_notifications_black_24dp.xml
create mode 100644 app/src/main/res/layout/activity_bottom.xml
create mode 100644 app/src/main/res/layout/activity_main.xml
create mode 100644 app/src/main/res/layout/fragment_dashboard.xml
create mode 100644 app/src/main/res/layout/fragment_home.xml
create mode 100644 app/src/main/res/layout/fragment_notifications.xml
create mode 100644 app/src/main/res/menu/bottom_nav_menu.xml
create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher.xml
create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp
create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp
create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp
create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
create mode 100644 app/src/main/res/mipmap-xxxhdpi/search.png
create mode 100644 app/src/main/res/mipmap-xxxhdpi/takepic.png
create mode 100644 app/src/main/res/navigation/mobile_navigation.xml
create mode 100644 app/src/main/res/values-night/themes.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/strings.xml
create mode 100644 app/src/main/res/values/themes.xml
create mode 100644 app/src/main/res/xml/backup_rules.xml
create mode 100644 app/src/main/res/xml/data_extraction_rules.xml
create mode 100644 app/src/main/res/xml/network_security_config.xml
create mode 100644 app/src/test/java/com/xypower/wpywapp/ExampleUnitTest.java
create mode 100644 app/src/test/java/com/xypower/wpywapp/StringUtils.java
create mode 100644 build.gradle
create mode 100644 gradle.properties
create mode 100644 gradlew
create mode 100644 gradlew.bat
create mode 100644 settings.gradle
create mode 100644 sign/platform.jks
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..977b79d
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,107 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ namespace 'com.xypower.wpywapp'
+ compileSdk 33
+
+ defaultConfig {
+ applicationId "com.xypower.wpywapp"
+ minSdk 28
+ targetSdk 33
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ signingConfigs {
+ debug {
+ storeFile file("../sign/platform.jks")
+ storePassword 'dowse_mtk8788'
+ keyAlias 'ds_mt8788_key'
+ keyPassword 'dowse_mtk8788'
+ }
+
+ release {
+ storeFile file("../sign/platform.jks")
+ storePassword 'dowse_mtk8788'
+ keyAlias 'ds_mt8788_key'
+ keyPassword 'dowse_mtk8788'
+ }
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ /*
+*支持databinding
+* */
+ dataBinding {
+ enabled = true
+ }
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+dependencies {
+ implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.8.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
+ implementation 'androidx.navigation:navigation-fragment:2.5.3'
+ implementation 'androidx.navigation:navigation-ui:2.5.3'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ implementation 'me.jessyan:autosize:1.2.1'
+
+ //viewmodel依赖
+ implementation "androidx.lifecycle:lifecycle-viewmodel:2.4.0"
+ implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
+//Livedata去除粘性事件
+ implementation 'com.kunminx.arch:unpeek-livedata:7.2.0-beta1'
+
+ implementation "com.tananaev:adblib:1.3"
+
+
+
+ // Add this library
+// implementation 'com.github.MuntashirAkon:libadb-android:1.0.1'
+
+ // Library to generate X509Certificate. You can also use BouncyCastle for
+ // this. See example for use-case.
+ implementation 'com.github.MuntashirAkon:sun-security-android:1.1'
+
+ // Bypass hidden API if you want to use the Android default Conscrypt in
+ // Android 9 (Pie) or later. It also requires additional steps. See
+ // https://github.com/LSPosed/AndroidHiddenApiBypass to find out more about
+ // this.
+ implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:2.0'
+
+ // Use custom Conscrypt library. If you want to connect to a remote ADB
+ // daemon instead of the device the app is currently running or do not want
+ // to bypass hidden API, this is the recommended choice.
+ implementation 'org.conscrypt:conscrypt-android:2.5.2'
+ implementation 'com.github.MuntashirAkon.spake2-java:android:2.0.0'
+ implementation 'org.bouncycastle:bcprov-jdk15to18:1.78'
+
+ implementation 'com.tananaev:adblib:1.3'
+
+
+ api 'com.github.KunMinX:MVI-Dispatcher:7.6.0'
+// api 'com.github.KunMinX.Strict-DataBinding:strict_databinding:6.2.0'
+
+ //Livedata去除粘性事件
+ implementation 'com.kunminx.arch:unpeek-livedata:7.8.0'
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/xypower/wpywapp/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/xypower/wpywapp/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..8f6add9
--- /dev/null
+++ b/app/src/androidTest/java/com/xypower/wpywapp/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.xypower.wpywapp;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.xypower.wpywapp", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f9058c8
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/xypower/wpywapp/ADBManager.java b/app/src/main/java/com/xypower/wpywapp/ADBManager.java
new file mode 100644
index 0000000..3def1d6
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/ADBManager.java
@@ -0,0 +1,20 @@
+package com.xypower.wpywapp;
+
+public class ADBManager {
+ private static final String ADB_PATH = "/path/to/adb"; // 指定adb的路径
+
+ public static void connect() {
+ // 执行adb连接命令
+ executeCommand(ADB_PATH + " connect :");
+ }
+
+ public static void disconnect() {
+ // 执行adb断开连接命令
+ executeCommand(ADB_PATH + " disconnect :");
+ }
+
+ private static void executeCommand(String command) {
+ // 执行adb命令的逻辑
+
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/MainActivity.java b/app/src/main/java/com/xypower/wpywapp/MainActivity.java
new file mode 100644
index 0000000..54583fd
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/MainActivity.java
@@ -0,0 +1,398 @@
+package com.xypower.wpywapp;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.databinding.DataBindingUtil;
+
+import android.content.Intent;
+import android.location.Location;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
+
+import com.tananaev.adblib.AdbConnection;
+import com.tananaev.adblib.AdbCrypto;
+import com.tananaev.adblib.AdbStream;
+import com.xypower.wpywapp.bean.TerminalBean;
+import com.xypower.wpywapp.databinding.ActivityMainBinding;
+import com.xypower.wpywapp.interfaces.MainActCallback;
+import com.xypower.wpywapp.jadb.JadbDevice;
+import com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbConnectionManager;
+import com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbInputStream;
+import com.xypower.wpywapp.tool.LocationUtil;
+import com.xypower.wpywapp.ui.BottomActivity;
+
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.Socket;
+
+
+/*
+这三个文件夹下日志的清除
+* /var/log/xymp/xymanagerLogs
+* /var/log/xymp/logs
+* /usr/local/share/tomcat/logs/
+
+
+
+这个文件日志的切割
+/usr/local/share/tomcat/logs/catalina.out
+*
+* */
+public class MainActivity extends AppCompatActivity {
+
+ private JadbDevice jadbDevice;
+ private TerminalBean bean = new TerminalBean();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
+ bean.setVerion("未知");
+ bean.setSim("未知");
+ bean.setPower("未知");
+ binding.setBean(bean);
+ Handler handler = new Handler() {
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ super.handleMessage(msg);
+ if (msg.what == 1) {
+ Toast.makeText(MainActivity.this, "1111", Toast.LENGTH_SHORT).show();
+ } else if (msg.what == 2) {
+ Toast.makeText(MainActivity.this, "2222", Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(MainActivity.this, "3333", Toast.LENGTH_SHORT).show();
+ }
+ }
+ };
+ binding.setCallback(new MainActCallback() {
+ @Override
+ public void link(View view) {
+
+ LocationUtil.register(MainActivity.this, 0, 0, new LocationUtil.OnLocationChangeListener() {
+ @Override
+ public void getLastKnownLocation(Location location) {
+ Log.e("xyh", "onLocationChanged: " + location.getLatitude());
+ }
+
+ @Override
+ public void onLocationChanged(Location location) {
+ //位置信息变化时触发
+ Log.e("xyh", "定位方式:" + location.getProvider());
+ Log.e("xyh", "纬度:" + location.getLatitude());
+ Log.e("xyh", "经度:" + location.getLongitude());
+ Log.e("xyh", "海拔:" + location.getAltitude());
+ Log.e("xyh", "时间:" + location.getTime());
+ Log.e("xyh", "国家:" + LocationUtil.getCountryName(MainActivity.this, location.getLatitude(), location.getLongitude()));
+ Log.e("xyh", "获取地理位置:" + LocationUtil.getAddress(MainActivity.this, location.getLatitude(), location.getLongitude()));
+ Log.e("xyh", "所在地:" + LocationUtil.getLocality(MainActivity.this, location.getLatitude(), location.getLongitude()));
+ Log.e("xyh", "所在街道:" + LocationUtil.getStreet(MainActivity.this, location.getLatitude(), location.getLongitude()));
+
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ System.out.println("dfsad");
+ }
+ });
+
+
+ //连接设备
+
+// Intent intent = new Intent(MainActivity.this, BottomActivity.class);
+// startActivity(intent);
+// String s = binding.ip.getText().toString();
+// if (RegexUtil.checkIpAddress(s)) {
+ //获取可adb调试的设备列表
+// new Thread(new Runnable() {
+// @Override
+// public void run() {
+//// try {
+////// Process process = Runtime.getRuntime().exec("adb shell devices");
+////// int i = process.waitFor();
+////// if (i == 0) {
+////// InputStream inputStream = process.getInputStream();
+////// System.out.println("2312");
+////// } else {
+////// InputStream errorStream = process.getErrorStream();
+////// System.out.println("888484");
+////// }
+////// AdbServer adbServer = new AdbServer(new AdbResponder() {
+////// @Override
+////// public void onCommand(String command) {
+////// Toast.makeText(MainActivity.this,"cehgnogn1",Toast.LENGTH_SHORT).show();
+////// }
+//////
+////// @Override
+////// public int getVersion() {
+////// Toast.makeText(MainActivity.this,"cehgnogn3",Toast.LENGTH_SHORT).show();
+////// return 0;
+////// }
+//////
+////// @Override
+////// public List getDevices() {
+////// Toast.makeText(MainActivity.this,"cehgnogn2",Toast.LENGTH_SHORT).show();
+////// return null;
+////// }
+////// });
+////// Runnable responder = adbServer.createResponder(new Socket("127.0.0.1", 15037));
+////// responder.run();
+////// JadbConnection jadb = new JadbConnection("192.168.0.100",5555);
+//// JadbConnection jadb = new JadbConnection("192.168.0.118", 5555);
+////// JadbConnection jadb = new JadbConnection("127.0.0.1", 5555);
+////// InetSocketAddress inetSocketAddress = jadb.connectToTcpDevice(new InetSocketAddress("192.168.0.101",5555));
+////// Message msg1 = new Message();
+////// msg1.what = 3;
+////// handler.sendMessage(msg1);
+////// System.out.println(inetSocketAddress);
+//// List devices = jadb.getDevices();
+//// jadbDevice = devices.get(0);
+////// jadbDevice.pull();
+//// } catch (IOException e) {
+//// throw new RuntimeException(e);
+//// } catch (JadbException e) {
+//// throw new RuntimeException(e);
+////// } catch (ConnectionToRemoteDeviceException e) {
+////// throw new RuntimeException(e);
+//// }
+//
+//
+//
+//// JadbConnection jadbConnection = new JadbConnection();
+//// List anyDevice = null;
+//// try {
+//// InetSocketAddress inetSocketAddress = jadbConnection.connectToTcpDevice(new InetSocketAddress("192.168.0.118",5555));
+//// List devices = jadbConnection.getDevices();
+//// JadbDevice jadbDevice = devices.get(1);
+//// RemoteFile remoteFile = new RemoteFile("/storage/emulated/0/iport_log.txt");
+//// String absolutePath = getFilesDir().getAbsolutePath();
+//// jadbDevice.pull(remoteFile,new File(absolutePath+"/a.txt"));
+//// System.out.println(inetSocketAddress);
+//// System.out.println(devices);
+////// anyDevice = jadbConnection.getDevices();
+//// } catch (IOException e) {
+//// e.printStackTrace();
+//// } catch (JadbException e) {
+//// e.printStackTrace();
+////// } catch (ConnectionToRemoteDeviceException e) {
+//// e.printStackTrace();
+//// } catch (ConnectionToRemoteDeviceException e) {
+//// e.printStackTrace();
+////
+//// }
+//
+//
+//// try {
+//// if(Runtime.getRuntime().exec(new String[]{"adb", "version"}).waitFor()!=0){
+//// System.out.println("dfsd");
+//// }
+//// } catch (IOException e) {
+//// throw new RuntimeException(e);
+//// } catch (InterruptedException e) {
+//// throw new RuntimeException(e);
+//// }
+//
+//
+// }
+// }).start();
+// } else {
+// Toast.makeText(MainActivity.this, "IP不合法", Toast.LENGTH_SHORT).show();
+// }
+// asyncTask.execute();
+ }
+
+ @Override
+ public void getInfo(View view) {
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+// ADBService adbService = new ADBServiceImpl();
+//// adbService.pullFile()
+// try {
+// List connectedDevicesUdid = adbService.getConnectedDevicesUdid();
+// System.out.println(connectedDevicesUdid);
+// } catch (Exception e) {
+// throw new RuntimeException(e);
+// }
+
+
+ try {
+ boolean connect = AdbConnectionManager.getInstance(MainActivity.this).connect("192.168.50.109", 5555);
+ if (connect) {
+// /storage/emulated/0/i1/log/2024_04_21_yunWei.log
+ String absolutePath = getFilesDir().getAbsolutePath();
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream1 = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell: exit");
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("pull: /storage/emulated/0/i1/log/2024_04_21_yunWei.log "+absolutePath+"/");
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell: date");
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbConnection adbConnection = AdbConnectionManager.getInstance(MainActivity.this).getAdbConnection();
+// ("pull: /storage/emulated/0/i1/log/2024_04_21_yunWei.log "+absolutePath+"/");
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream open = adbConnection.open("sync: recv /storage/emulated/0/i1/iport_log.txt "+absolutePath+"/");
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("sync: recv: /storage/emulated/0/i1/iport_log.txt "+absolutePath+"/");
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell:cat /storage/emulated/0/iport_log.txt");
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell:cat /storage/emulated/0/iport_log.txt");
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell:cp /storage/emulated/0/iport_log.txt "+absolutePath+"/aa.txt");
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream2 = AdbConnectionManager.getInstance(MainActivity.this).openStream("sync:");
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream1 = AdbConnectionManager.getInstance(MainActivity.this).openStream(LocalServices.SYNC);
+// adbStream1.flush();
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell: sync send /storage/emulated/0/ "+absolutePath+"/324.txt");
+// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("sync:"+"\n" +"recv: /storage/emulated/0/i1/iport_log.txt");
+ com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("sync:"+"\n" +"recv: /storage/emulated/0/i1/iport_log.txt");
+// byte[] b = new byte[4];
+// AdbInputStream adbInputStream = adbStream.openInputStream();
+ AdbInputStream adbInputStream = adbStream.openInputStream();
+
+//
+
+ String result = null;
+ BufferedReader reader = new BufferedReader(new InputStreamReader(adbInputStream));
+ String line = null;
+ StringBuffer sb = new StringBuffer();
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ result = sb.toString();
+
+// int off = 0;
+// int n = 0;
+// int len = b.length;
+// while (n < len) {
+// int count = adbInputStream.read(b, off + n, len - n);
+// if (count < 0)
+// throw new EOFException();
+// n += count;
+// }
+
+ System.out.println(result);
+ }
+ } catch (Exception e) {
+ try {
+ AdbConnectionManager.getInstance(MainActivity.this).disconnect();
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ throw new RuntimeException(e);
+ }
+
+// try {
+// Process process = Runtime.getRuntime().exec("version");
+// int i = process.waitFor();
+// if (i == 0) {
+// InputStream inputStream = process.getInputStream();
+// System.out.println("2312");
+// } else {
+// InputStream errorStream = process.getErrorStream();
+// System.out.println("888484");
+// }
+//// JadbConnection jadb = new JadbConnection(s, 5555);
+//// List devices = jadb.getDevices();
+//// jadbDevice = devices.get(0);
+// } catch (IOException e) {
+// throw new RuntimeException(e);
+// } catch (InterruptedException e) {
+// throw new RuntimeException(e);
+// }
+ }
+ }).start();
+
+ //获取设备信息
+// try {
+// if (jadbDevice != null) {
+// jadbDevice.pull(new RemoteFile("/path/to/file.txt"), new File("file.txt"));
+// //解析文件 将文件的值赋给bean对象
+// binding.setBean(bean);
+// }
+// } catch (IOException e) {
+// throw new RuntimeException(e);
+// } catch (JadbException e) {
+// throw new RuntimeException(e);
+// }
+ }
+
+ @Override
+ public void takePic(View view) {
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+
+
+ Socket socket = null;
+ AdbCrypto crypto = new AdbCrypto();
+ AdbConnection adbConnection = null;
+ try {
+// Message msg = new Message();
+// msg.what = 1;
+// handler.sendMessage(msg);
+ socket = new Socket("192.168.0.101", 5037);
+// Message msg1 = new Message();
+// msg1.what = 3;
+// handler.sendMessage(msg1);
+// handler.sendMessage(new Message());
+ adbConnection = AdbConnection.create(socket, crypto);
+ adbConnection.connect();
+ Message msg1 = new Message();
+ msg1.what = 3;
+ handler.sendMessage(msg1);
+ System.out.println("dsadADas");
+ AdbStream fsda = adbConnection.open("devices");
+ System.out.println("rewrewrew");
+ } catch (IOException e) {
+ Message msg = new Message();
+ msg.what = 2;
+ handler.sendMessage(msg);
+// throw new RuntimeException(e);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }).start();
+
+
+// CommandResult adbDevices = ShellUtils.execCommand("adb version", false);
+// System.out.println(adbDevices.errorMsg);
+// testCmd.runAdbCommand("version");
+// System.out.println(s);
+// boolean b = testCmd.haveRoot();
+// if (b) {
+//
+// }
+// if (jadbDevice != null) {
+ //手动拍照
+// jadbDevice.buildCmdLine("手动拍照");
+// }
+ }
+ });
+
+
+ }
+
+ public boolean runRootCommand(String command) {
+ Process process = null;
+ DataOutputStream os = null;
+ try {
+ process = Runtime.getRuntime().exec("su");
+ os = new DataOutputStream(process.getOutputStream());
+ os.writeBytes(command + "\n");
+ os.writeBytes("exit\n");
+ os.flush();
+ process.waitFor();
+ } catch (Exception e) {
+ return false;
+ } finally {
+ try {
+ if (os != null) {
+ os.close();
+ }
+ process.destroy();
+ } catch (Exception e) {
+ }
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/xypower/wpywapp/WpywApplication.java b/app/src/main/java/com/xypower/wpywapp/WpywApplication.java
new file mode 100644
index 0000000..c86c30b
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/WpywApplication.java
@@ -0,0 +1,47 @@
+package com.xypower.wpywapp;
+
+import android.app.ActivityManager;
+import android.app.Application;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModelStore;
+import androidx.lifecycle.ViewModelStoreOwner;
+
+public class WpywApplication extends Application implements ViewModelStoreOwner {
+ private ViewModelStore mAppViewModelStore;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mAppViewModelStore = new ViewModelStore();
+ }
+
+ @NonNull
+ @Override
+ public ViewModelStore getViewModelStore() {
+ return mAppViewModelStore;
+ }
+
+ /**
+ * 判断是否是UI进程
+ *
+ * @return
+ */
+ public boolean isUIProcess() {
+ int pid = android.os.Process.myPid();
+ String processName = "";
+ ActivityManager activityManager = (ActivityManager) getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
+ for (ActivityManager.RunningAppProcessInfo runningAppProcessInfo : activityManager.getRunningAppProcesses()) {
+ if (runningAppProcessInfo.pid == pid) {
+ processName = runningAppProcessInfo.processName;
+ break;
+ }
+ }
+ return TextUtils.equals(getPackageName(), processName);
+ }
+
+
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/base/BaseActivity.java b/app/src/main/java/com/xypower/wpywapp/base/BaseActivity.java
new file mode 100644
index 0000000..d8b6c38
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/base/BaseActivity.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2018-present KunMinX
+ *
+ * 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.
+ */
+
+package com.xypower.wpywapp.base;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.inputmethod.InputMethodManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModel;
+
+import com.kunminx.architecture.ui.scope.ViewModelScope;
+import com.xypower.wpywapp.page.DataBindingActivity;
+
+/**
+ * Create by KunMinX at 19/8/1
+ */
+public abstract class BaseActivity extends DataBindingActivity {
+
+ private final ViewModelScope mViewModelScope = new ViewModelScope();
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+
+
+ super.onCreate(savedInstanceState);
+
+
+ //TODO tip 1: DataBinding 严格模式(详见 DataBindingActivity - - - - - ):
+ // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露,
+ // 通过这方式,彻底解决 View 实例 Null 安全一致性问题,
+ // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。
+
+ // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910
+ }
+
+ //TODO tip 2: Jetpack 通过 "工厂模式" 实现 ViewModel 作用域可控,
+ //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域,
+ //值得注意的是,通过不同作用域 Provider 获得 ViewModel 实例非同一个,
+ //故若 ViewModel 状态信息保留不符合预期,可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。
+
+ //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6257931840
+
+ protected T getActivityScopeViewModel(@NonNull Class modelClass) {
+ return mViewModelScope.getActivityScopeViewModel(this, modelClass);
+ }
+
+ protected T getApplicationScopeViewModel(@NonNull Class modelClass) {
+ return mViewModelScope.getApplicationScopeViewModel(modelClass);
+ }
+
+
+ protected void toggleSoftInput() {
+ InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE);
+ imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+
+ protected void openUrlInBrowser(String url) {
+ Uri uri = Uri.parse(url);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ startActivity(intent);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/base/BaseFragment.java b/app/src/main/java/com/xypower/wpywapp/base/BaseFragment.java
new file mode 100644
index 0000000..9bcbaf5
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/base/BaseFragment.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018-present KunMinX
+ *
+ * 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.
+ */
+
+package com.xypower.wpywapp.base;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.view.inputmethod.InputMethodManager;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModel;
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.NavHostFragment;
+
+import com.kunminx.architecture.ui.scope.ViewModelScope;
+import com.xypower.wpywapp.page.DataBindingFragment;
+
+/**
+ * Create by KunMinX at 19/7/11
+ */
+public abstract class BaseFragment extends DataBindingFragment {
+
+ private final ViewModelScope mViewModelScope = new ViewModelScope();
+
+ //TODO tip 1: DataBinding 严格模式(详见 DataBindingFragment - - - - - ):
+ // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露,
+ // 通过这方式,彻底解决 View 实例 Null 安全一致性问题,
+ // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。
+
+ // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910
+
+ //TODO tip 2: Jetpack 通过 "工厂模式" 实现 ViewModel 作用域可控,
+ //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域,
+ //值得注意的是,通过不同作用域 Provider 获得 ViewModel 实例非同一个,
+ //故若 ViewModel 状态信息保留不符合预期,可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。
+
+ //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6257931840
+
+ protected T getFragmentScopeViewModel(@NonNull Class modelClass) {
+ return mViewModelScope.getFragmentScopeViewModel(this, modelClass);
+ }
+
+ protected T getActivityScopeViewModel(@NonNull Class modelClass) {
+ return mViewModelScope.getActivityScopeViewModel(mActivity, modelClass);
+ }
+
+ protected T getApplicationScopeViewModel(@NonNull Class modelClass) {
+ return mViewModelScope.getApplicationScopeViewModel(modelClass);
+ }
+
+ protected NavController nav() {
+ return NavHostFragment.findNavController(this);
+ }
+
+ protected void toggleSoftInput() {
+ InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Activity.INPUT_METHOD_SERVICE);
+ imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+
+ protected void openUrlInBrowser(String url) {
+ Uri uri = Uri.parse(url);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ startActivity(intent);
+ }
+
+ protected Context getApplicationContext() {
+ return mActivity.getApplicationContext();
+ }
+
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/base/BaseViewModel.java b/app/src/main/java/com/xypower/wpywapp/base/BaseViewModel.java
new file mode 100644
index 0000000..f949398
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/base/BaseViewModel.java
@@ -0,0 +1,10 @@
+package com.xypower.wpywapp.base;
+
+import androidx.lifecycle.ViewModel;
+
+public class BaseViewModel extends ViewModel {
+ @Override
+ protected void onCleared() {
+ super.onCleared();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/xypower/wpywapp/bean/TerminalBean.java b/app/src/main/java/com/xypower/wpywapp/bean/TerminalBean.java
new file mode 100644
index 0000000..0c142b8
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/bean/TerminalBean.java
@@ -0,0 +1,32 @@
+package com.xypower.wpywapp.bean;
+
+public class TerminalBean {
+
+ private String verion;
+ private String sim;
+ private String power;
+
+ public String getVerion() {
+ return verion;
+ }
+
+ public void setVerion(String verion) {
+ this.verion = verion;
+ }
+
+ public String getSim() {
+ return sim;
+ }
+
+ public void setSim(String sim) {
+ this.sim = sim;
+ }
+
+ public String getPower() {
+ return power;
+ }
+
+ public void setPower(String power) {
+ this.power = power;
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/interfaces/MainActCallback.java b/app/src/main/java/com/xypower/wpywapp/interfaces/MainActCallback.java
new file mode 100644
index 0000000..1fdbb47
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/interfaces/MainActCallback.java
@@ -0,0 +1,15 @@
+package com.xypower.wpywapp.interfaces;
+
+import android.view.View;
+
+import java.io.IOException;
+
+public interface MainActCallback {
+
+ void link(View view);
+
+ void getInfo(View view);
+
+ void takePic(View view);
+
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterInputStream.java b/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterInputStream.java
new file mode 100644
index 0000000..05f1da4
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterInputStream.java
@@ -0,0 +1,47 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AdbFilterInputStream extends FilterInputStream {
+ public AdbFilterInputStream(InputStream inputStream) {
+ super(inputStream);
+ }
+
+ @Override
+ public int read() throws IOException {
+ int b1 = in.read();
+ if (b1 == 0x0d) {
+ in.mark(1);
+ int b2 = in.read();
+ if (b2 == 0x0a) {
+ return b2;
+ }
+ in.reset();
+ }
+ return b1;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) throws IOException {
+ int n = 0;
+ for (int i = 0; i < length; i++) {
+ int b = read();
+ if (b == -1) return n == 0 ? -1 : n;
+ buffer[offset + n] = (byte) b;
+ n++;
+
+ // Return as soon as no more data is available (and at least one byte was read)
+ if (in.available() <= 0) {
+ return n;
+ }
+ }
+ return n;
+ }
+
+ @Override
+ public int read(byte[] buffer) throws IOException {
+ return read(buffer, 0, buffer.length);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterOutputStream.java b/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterOutputStream.java
new file mode 100644
index 0000000..d8432d3
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterOutputStream.java
@@ -0,0 +1,21 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class AdbFilterOutputStream extends LookBackFilteringOutputStream {
+ public AdbFilterOutputStream(OutputStream inner) {
+ super(inner, 1);
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ if (!lookback().isEmpty()) {
+ Byte last = lookback().getFirst();
+ if (last != null && last == 0x0d && c == 0x0a) {
+ unwrite();
+ }
+ }
+ super.write(c);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/AdbServerLauncher.java b/app/src/main/java/com/xypower/wpywapp/jadb/AdbServerLauncher.java
new file mode 100644
index 0000000..244892c
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/AdbServerLauncher.java
@@ -0,0 +1,48 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Launches the ADB server
+ */
+public class AdbServerLauncher {
+ private final String executable;
+ private Subprocess subprocess;
+
+ /**
+ * Creates a new launcher loading ADB from the environment.
+ *
+ * @param subprocess the sub-process.
+ * @param environment the environment to use to locate the ADB executable.
+ */
+ public AdbServerLauncher(Subprocess subprocess, Map environment) {
+ this(subprocess, findAdbExecutable(environment));
+ }
+
+ /**
+ * Creates a new launcher with the specified ADB.
+ *
+ * @param subprocess the sub-process.
+ * @param executable the location of the ADB executable.
+ */
+ public AdbServerLauncher(Subprocess subprocess, String executable) {
+ this.subprocess = subprocess;
+ this.executable = executable;
+ }
+
+ private static String findAdbExecutable(Map environment) {
+ String androidHome = environment.get("ANDROID_HOME");
+ if (androidHome == null || androidHome.equals("")) {
+ return "adb";
+ }
+ return androidHome + "/platform-tools/adb";
+ }
+
+ public void launch() throws IOException, InterruptedException {
+ Process p = subprocess.execute(new String[]{executable, "start-server"});
+ p.waitFor();
+ int exitValue = p.exitValue();
+ if (exitValue != 0) throw new IOException("adb exited with exit code: " + exitValue);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/ConnectionToRemoteDeviceException.java b/app/src/main/java/com/xypower/wpywapp/jadb/ConnectionToRemoteDeviceException.java
new file mode 100644
index 0000000..5f513c9
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/ConnectionToRemoteDeviceException.java
@@ -0,0 +1,7 @@
+package com.xypower.wpywapp.jadb;
+
+public class ConnectionToRemoteDeviceException extends Exception {
+ public ConnectionToRemoteDeviceException(String message) {
+ super(message);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/DeviceDetectionListener.java b/app/src/main/java/com/xypower/wpywapp/jadb/DeviceDetectionListener.java
new file mode 100644
index 0000000..93adb18
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/DeviceDetectionListener.java
@@ -0,0 +1,9 @@
+package com.xypower.wpywapp.jadb;
+
+import java.util.List;
+
+public interface DeviceDetectionListener {
+ void onDetect(List devices);
+ void onException(Exception e);
+}
+
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/DeviceWatcher.java b/app/src/main/java/com/xypower/wpywapp/jadb/DeviceWatcher.java
new file mode 100644
index 0000000..db57ae8
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/DeviceWatcher.java
@@ -0,0 +1,44 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.IOException;
+
+public class DeviceWatcher implements Runnable {
+ private Transport transport;
+ private final DeviceDetectionListener listener;
+ private final JadbConnection connection;
+
+ public DeviceWatcher(Transport transport, DeviceDetectionListener listener, JadbConnection connection) {
+ this.transport = transport;
+ this.listener = listener;
+ this.connection = connection;
+ }
+
+ @Override
+ public void run() {
+ watch();
+ }
+
+ @SuppressWarnings("squid:S2189") // watcher is stopped by closing transport
+ private void watch() {
+ try {
+ while (true) {
+ listener.onDetect(connection.parseDevices(transport.readString()));
+ }
+ } catch (IOException ioe) {
+ synchronized(this) {
+ if (transport != null) {
+ listener.onException(ioe);
+ }
+ }
+ } catch (Exception e) {
+ listener.onException(e);
+ }
+ }
+
+ public void stop() throws IOException {
+ synchronized(this) {
+ transport.close();
+ transport = null;
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectToRemoteTcpDevice.java b/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectToRemoteTcpDevice.java
new file mode 100644
index 0000000..4a7cc7c
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectToRemoteTcpDevice.java
@@ -0,0 +1,29 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+class HostConnectToRemoteTcpDevice extends HostConnectionCommand {
+ HostConnectToRemoteTcpDevice(Transport transport) {
+ super(transport, new ResponseValidatorImp());
+ }
+
+ //Visible for testing
+ HostConnectToRemoteTcpDevice(Transport transport, ResponseValidator responseValidator) {
+ super(transport, responseValidator);
+ }
+
+ InetSocketAddress connect(InetSocketAddress inetSocketAddress)
+ throws IOException, JadbException, ConnectionToRemoteDeviceException {
+ return executeHostCommand("connect", inetSocketAddress);
+ }
+
+ static final class ResponseValidatorImp extends ResponseValidatorBase {
+ private static final String SUCCESSFULLY_CONNECTED = "connected to";
+ private static final String ALREADY_CONNECTED = "already connected to";
+
+ ResponseValidatorImp() {
+ super(SUCCESSFULLY_CONNECTED, ALREADY_CONNECTED);
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectionCommand.java b/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectionCommand.java
new file mode 100644
index 0000000..c007df2
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectionCommand.java
@@ -0,0 +1,70 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+public class HostConnectionCommand {
+ private final Transport transport;
+ private final ResponseValidator responseValidator;
+
+ HostConnectionCommand(Transport transport, ResponseValidator responseValidator) {
+ this.transport = transport;
+ this.responseValidator = responseValidator;
+ }
+
+ InetSocketAddress executeHostCommand(String command, InetSocketAddress inetSocketAddress)
+ throws IOException, JadbException, ConnectionToRemoteDeviceException {
+ transport.send(String.format("host:%s:%s:%d", command, inetSocketAddress.getHostString(), inetSocketAddress.getPort()));
+ verifyTransportLevel();
+ verifyProtocolLevel();
+
+ return inetSocketAddress;
+ }
+
+ private void verifyTransportLevel() throws IOException, JadbException {
+ transport.verifyResponse();
+ }
+
+ private void verifyProtocolLevel() throws IOException, ConnectionToRemoteDeviceException {
+ String status = transport.readString();
+ responseValidator.validate(status);
+ }
+
+ //@VisibleForTesting
+ interface ResponseValidator {
+ void validate(String response) throws ConnectionToRemoteDeviceException;
+ }
+
+ static class ResponseValidatorBase implements ResponseValidator {
+ private final String successMessage;
+ private final String errorMessage;
+
+ ResponseValidatorBase(String successMessage, String errorMessage) {
+ this.successMessage = successMessage;
+ this.errorMessage = errorMessage;
+ }
+
+ public void validate(String response) throws ConnectionToRemoteDeviceException {
+ if (!checkIfConnectedSuccessfully(response) && !checkIfAlreadyConnected(response)) {
+ throw new ConnectionToRemoteDeviceException(extractError(response));
+ }
+ }
+
+ private boolean checkIfConnectedSuccessfully(String response) {
+ return response.startsWith(successMessage);
+ }
+
+ private boolean checkIfAlreadyConnected(String response) {
+ return response.startsWith(errorMessage);
+ }
+
+ private String extractError(String response) {
+ int lastColon = response.lastIndexOf(':');
+ if (lastColon != -1) {
+ return response.substring(lastColon);
+ } else {
+ return response;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/HostDisconnectFromRemoteTcpDevice.java b/app/src/main/java/com/xypower/wpywapp/jadb/HostDisconnectFromRemoteTcpDevice.java
new file mode 100644
index 0000000..7e01dc5
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/HostDisconnectFromRemoteTcpDevice.java
@@ -0,0 +1,30 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+public class HostDisconnectFromRemoteTcpDevice extends HostConnectionCommand {
+ HostDisconnectFromRemoteTcpDevice(Transport transport) {
+ super(transport, new ResponseValidatorImp());
+
+ }
+
+ //Visible for testing
+ HostDisconnectFromRemoteTcpDevice(Transport transport, ResponseValidator responseValidator) {
+ super(transport, responseValidator);
+ }
+
+ InetSocketAddress disconnect(InetSocketAddress inetSocketAddress)
+ throws IOException, JadbException, ConnectionToRemoteDeviceException {
+ return executeHostCommand("disconnect", inetSocketAddress);
+ }
+
+ static final class ResponseValidatorImp extends ResponseValidatorBase {
+ private static final String SUCCESSFULLY_DISCONNECTED = "disconnected";
+ private static final String ALREADY_DISCONNECTED = "error: no such device";
+
+ ResponseValidatorImp() {
+ super(SUCCESSFULLY_DISCONNECTED, ALREADY_DISCONNECTED);
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/ITransportFactory.java b/app/src/main/java/com/xypower/wpywapp/jadb/ITransportFactory.java
new file mode 100644
index 0000000..36eba22
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/ITransportFactory.java
@@ -0,0 +1,10 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.IOException;
+
+/**
+ * Created by Törcsi on 2016. 03. 01..
+ */
+public interface ITransportFactory {
+ Transport createTransport() throws IOException;
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/JadbConnection.java b/app/src/main/java/com/xypower/wpywapp/jadb/JadbConnection.java
new file mode 100644
index 0000000..881bb8d
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/JadbConnection.java
@@ -0,0 +1,83 @@
+package com.xypower.wpywapp.jadb;
+
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+
+public class JadbConnection implements ITransportFactory {
+
+ private final String host;
+ private final int port;
+
+ private static final int DEFAULTPORT = 5037;
+
+ public JadbConnection() {
+ this("localhost", DEFAULTPORT);
+ }
+
+ public JadbConnection(String host, int port) {
+ this.host = host;
+ this.port = port;
+ }
+
+ public Transport createTransport() throws IOException {
+ return new Transport(new Socket(host, port));
+ }
+
+ public String getHostVersion() throws IOException, JadbException {
+ try (Transport transport = createTransport()) {
+ transport.send("host:version");
+ transport.verifyResponse();
+ return transport.readString();
+ }
+ }
+
+ public InetSocketAddress connectToTcpDevice(InetSocketAddress inetSocketAddress)
+ throws IOException, JadbException, ConnectionToRemoteDeviceException {
+ try (Transport transport = createTransport()) {
+ return new HostConnectToRemoteTcpDevice(transport).connect(inetSocketAddress);
+ }
+ }
+
+ public InetSocketAddress disconnectFromTcpDevice(InetSocketAddress tcpAddressEntity)
+ throws IOException, JadbException, ConnectionToRemoteDeviceException {
+ try (Transport transport = createTransport()) {
+ return new HostDisconnectFromRemoteTcpDevice(transport).disconnect(tcpAddressEntity);
+ }
+ }
+
+ public List getDevices() throws IOException, JadbException {
+ try (Transport transport = createTransport()) {
+ transport.send("host:devices");
+ transport.verifyResponse();
+ String body = transport.readString();
+ return parseDevices(body);
+ }
+ }
+
+ public DeviceWatcher createDeviceWatcher(DeviceDetectionListener listener) throws IOException, JadbException {
+ Transport transport = createTransport();
+ transport.send("host:track-devices");
+ transport.verifyResponse();
+ return new DeviceWatcher(transport, listener, this);
+ }
+
+ public List parseDevices(String body) {
+ String[] lines = body.split("\n");
+ ArrayList devices = new ArrayList<>(lines.length);
+ for (String line : lines) {
+ String[] parts = line.split("\t");
+ if (parts.length > 1) {
+ devices.add(new JadbDevice(parts[0], this)); // parts[1] is type
+ }
+ }
+ return devices;
+ }
+
+ public JadbDevice getAnyDevice() {
+ return JadbDevice.createAny(this);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/JadbDevice.java b/app/src/main/java/com/xypower/wpywapp/jadb/JadbDevice.java
new file mode 100644
index 0000000..a03bb69
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/JadbDevice.java
@@ -0,0 +1,256 @@
+package com.xypower.wpywapp.jadb;
+
+
+
+import com.xypower.wpywapp.jadb.managers.Bash;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class JadbDevice {
+ @SuppressWarnings("squid:S00115")
+ public enum State {
+ Unknown,
+ Offline,
+ Device,
+ Recovery,
+ BootLoader,
+ Unauthorized,
+ Authorizing,
+ Sideload,
+ Connecting,
+ Rescue
+ }
+
+ //noinspection OctalInteger
+ private static final int DEFAULT_MODE = 0664;
+ private final String serial;
+ private final ITransportFactory transportFactory;
+ private static final int DEFAULT_TCPIP_PORT = 5555;
+
+ JadbDevice(String serial, ITransportFactory tFactory) {
+ this.serial = serial;
+ this.transportFactory = tFactory;
+ }
+
+ static JadbDevice createAny(JadbConnection connection) {
+ return new JadbDevice(connection);
+ }
+
+ private JadbDevice(ITransportFactory tFactory) {
+ serial = null;
+ this.transportFactory = tFactory;
+ }
+
+ private State convertState(String type) {
+ switch (type) {
+ case "device": return State.Device;
+ case "offline": return State.Offline;
+ case "bootloader": return State.BootLoader;
+ case "recovery": return State.Recovery;
+ case "unauthorized": return State.Unauthorized;
+ case "authorizing" : return State.Authorizing;
+ case "connecting": return State.Connecting;
+ case "sideload": return State.Sideload;
+ case "rescue" : return State.Rescue;
+ default: return State.Unknown;
+ }
+ }
+
+ private Transport getTransport() throws IOException, JadbException {
+ Transport transport = transportFactory.createTransport();
+ // Do not use try-with-resources here. We want to return unclosed Transport and it is up to caller
+ // to close it. Here we close it only in case of exception.
+ try {
+ send(transport, serial == null ? "host:transport-any" : "host:transport:" + serial );
+ } catch (IOException|JadbException e) {
+ transport.close();
+ throw e;
+ }
+ return transport;
+ }
+
+ public String getSerial() {
+ return serial;
+ }
+
+ public State getState() throws IOException, JadbException {
+ try (Transport transport = transportFactory.createTransport()) {
+ send(transport, serial == null ? "host:get-state" : "host-serial:" + serial + ":get-state");
+ return convertState(transport.readString());
+ }
+ }
+
+ /**
Execute a shell command.
+ *
+ *
For Lollipop and later see: {@link #execute(String, String...)}
+ *
+ * @param command main command to run. E.g. "ls"
+ * @param args arguments to the command.
+ * @return combined stdout/stderr stream.
+ * @throws IOException
+ * @throws JadbException
+ */
+ public InputStream executeShell(String command, String... args) throws IOException, JadbException {
+ Transport transport = getTransport();
+ StringBuilder shellLine = buildCmdLine(command, args);
+ send(transport, "shell:" + shellLine.toString());
+ return new AdbFilterInputStream(new BufferedInputStream(transport.getInputStream()));
+ }
+
+ /**
+ *
+ * @deprecated Use InputStream executeShell(String command, String... args) method instead. Together with
+ * Stream.copy(in, out), it is possible to achieve the same effect.
+ */
+ @Deprecated
+ public void executeShell(OutputStream output, String command, String... args) throws IOException, JadbException {
+ try (Transport transport = getTransport()) {
+ StringBuilder shellLine = buildCmdLine(command, args);
+ send(transport, "shell:" + shellLine.toString());
+ if (output == null)
+ return;
+
+ AdbFilterOutputStream out = new AdbFilterOutputStream(output);
+ transport.readResponseTo(out);
+ }
+ }
+
+ /**
Execute a command with raw binary output.
+ *
+ *
Support for this command was added in Lollipop (Android 5.0), and is the recommended way to transmit binary
+ * data with that version or later. For earlier versions of Android, use
+ * {@link #executeShell(String, String...)}.
+ *
+ * @param command main command to run, e.g. "screencap"
+ * @param args arguments to the command, e.g. "-p".
+ * @return combined stdout/stderr stream.
+ * @throws IOException
+ * @throws JadbException
+ */
+ public InputStream execute(String command, String... args) throws IOException, JadbException {
+ Transport transport = getTransport();
+ StringBuilder shellLine = buildCmdLine(command, args);
+ send(transport, "exec:" + shellLine.toString());
+ return new BufferedInputStream(transport.getInputStream());
+ }
+
+ /**
+ * Builds a command line string from the command and its arguments.
+ *
+ * @param command the command.
+ * @param args the list of arguments.
+ * @return the command line.
+ */
+ private StringBuilder buildCmdLine(String command, String... args) {
+ StringBuilder shellLine = new StringBuilder(command);
+ for (String arg : args) {
+ shellLine.append(" ");
+ shellLine.append(Bash.quote(arg));
+ }
+ return shellLine;
+ }
+
+ /**
+ * Enable tcpip on the default port (5555)
+ *
+ * @return success or failure
+ */
+ public void enableAdbOverTCP() throws IOException, JadbException {
+ enableAdbOverTCP(DEFAULT_TCPIP_PORT);
+ }
+
+ /**
+ * Enable tcpip on a specific port
+ *
+ * @param port for the device to bind on
+ *
+ * @return success or failure
+ */
+ public void enableAdbOverTCP(int port) throws IOException, JadbException {
+ try (Transport transport = getTransport()) {
+ send(transport, String.format("tcpip:%d", port));
+ }
+ }
+
+ public List list(String remotePath) throws IOException, JadbException {
+ try (Transport transport = getTransport()) {
+ SyncTransport sync = transport.startSync();
+ sync.send("LIST", remotePath);
+
+ List result = new ArrayList<>();
+ for (RemoteFileRecord dent = sync.readDirectoryEntry(); dent != RemoteFileRecord.DONE; dent = sync.readDirectoryEntry()) {
+ result.add(dent);
+ }
+ return result;
+ }
+ }
+
+ public void push(InputStream source, long lastModified, int mode, RemoteFile remote) throws IOException, JadbException {
+ try (Transport transport = getTransport()) {
+ SyncTransport sync = transport.startSync();
+ sync.send("SEND", remote.getPath() + "," + mode);
+
+ sync.sendStream(source);
+
+ sync.sendStatus("DONE", (int) lastModified);
+ sync.verifyStatus();
+ }
+ }
+
+ public void push(File local, RemoteFile remote) throws IOException, JadbException {
+ try (FileInputStream fileStream = new FileInputStream(local)) {
+ push(fileStream, TimeUnit.MILLISECONDS.toSeconds(local.lastModified()), DEFAULT_MODE, remote);
+ }
+ }
+
+ public void pull(RemoteFile remote, OutputStream destination) throws IOException, JadbException {
+ try (Transport transport = getTransport()) {
+ SyncTransport sync = transport.startSync();
+ sync.send("RECV", remote.getPath());
+
+ sync.readChunksTo(destination);
+ }
+ }
+
+ public void pull(RemoteFile remote, File local) throws IOException, JadbException {
+ try (FileOutputStream fileStream = new FileOutputStream(local)) {
+ pull(remote, fileStream);
+ }
+ }
+
+ private void send(Transport transport, String command) throws IOException, JadbException {
+ transport.send(command);
+ transport.verifyResponse();
+ }
+
+ @Override
+ public String toString() {
+ return "Android Device with serial " + serial;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((serial == null) ? 0 : serial.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ JadbDevice other = (JadbDevice) obj;
+ if (serial == null) {
+ return other.serial == null;
+ }
+ return serial.equals(other.serial);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/JadbException.java b/app/src/main/java/com/xypower/wpywapp/jadb/JadbException.java
new file mode 100644
index 0000000..068612a
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/JadbException.java
@@ -0,0 +1,10 @@
+package com.xypower.wpywapp.jadb;
+
+public class JadbException extends Exception {
+
+ public JadbException(String message) {
+ super(message);
+ }
+
+ private static final long serialVersionUID = -3879283786835654165L;
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/LookBackFilteringOutputStream.java b/app/src/main/java/com/xypower/wpywapp/jadb/LookBackFilteringOutputStream.java
new file mode 100644
index 0000000..9f39fa8
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/LookBackFilteringOutputStream.java
@@ -0,0 +1,45 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayDeque;
+
+public class LookBackFilteringOutputStream extends FilterOutputStream {
+ private final ArrayDeque buffer;
+ private final int lookBackBufferSize;
+
+ protected LookBackFilteringOutputStream(OutputStream inner, int lookBackBufferSize)
+ {
+ super(inner);
+ this.lookBackBufferSize = lookBackBufferSize;
+ this.buffer = new ArrayDeque<>(lookBackBufferSize);
+ }
+
+ protected void unwrite() {
+ buffer.removeFirst();
+ }
+
+ protected ArrayDeque lookback() {
+ return buffer;
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ buffer.addLast((byte) c);
+ flushBuffer(lookBackBufferSize);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ flushBuffer(0);
+ out.flush();
+ }
+
+ private void flushBuffer(int size) throws IOException {
+ while (buffer.size() > size) {
+ Byte b = buffer.removeFirst();
+ out.write(b);
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFile.java b/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFile.java
new file mode 100644
index 0000000..dfb2313
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFile.java
@@ -0,0 +1,31 @@
+package com.xypower.wpywapp.jadb;
+
+/**
+ * Created by vidstige on 2014-03-20
+ */
+public class RemoteFile {
+ private final String path;
+
+ public RemoteFile(String path) { this.path = path; }
+
+ public String getName() { throw new UnsupportedOperationException(); }
+ public int getSize() { throw new UnsupportedOperationException(); }
+ public int getLastModified() { throw new UnsupportedOperationException(); }
+ public boolean isDirectory() { throw new UnsupportedOperationException(); }
+
+ public String getPath() { return path;}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ RemoteFile that = (RemoteFile) o;
+ return path.equals(that.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return path.hashCode();
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFileRecord.java b/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFileRecord.java
new file mode 100644
index 0000000..6775790
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFileRecord.java
@@ -0,0 +1,34 @@
+package com.xypower.wpywapp.jadb;
+
+/**
+ * Created by vidstige on 2014-03-19.
+ */
+class RemoteFileRecord extends RemoteFile {
+ public static final RemoteFileRecord DONE = new RemoteFileRecord(null, 0, 0, 0);
+
+ private final int mode;
+ private final int size;
+ private final int lastModified;
+
+ public RemoteFileRecord(String name, int mode, int size, int lastModified) {
+ super(name);
+ this.mode = mode;
+ this.size = size;
+ this.lastModified = lastModified;
+ }
+
+ @Override
+ public int getSize() {
+ return size;
+ }
+
+ @Override
+ public int getLastModified() {
+ return lastModified;
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return (mode & (1 << 14)) == (1 << 14);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/Stream.java b/app/src/main/java/com/xypower/wpywapp/jadb/Stream.java
new file mode 100644
index 0000000..7a91d2e
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/Stream.java
@@ -0,0 +1,27 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+public class Stream {
+ private Stream() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ public static void copy(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[1024 * 10];
+ int len;
+ while ((len = in.read(buffer)) != -1) {
+ out.write(buffer, 0, len);
+ }
+ }
+
+ public static String readAll(InputStream input, Charset charset) throws IOException {
+ ByteArrayOutputStream tmp = new ByteArrayOutputStream();
+ Stream.copy(input, tmp);
+ return new String(tmp.toByteArray(), charset);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/Subprocess.java b/app/src/main/java/com/xypower/wpywapp/jadb/Subprocess.java
new file mode 100644
index 0000000..9b52b55
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/Subprocess.java
@@ -0,0 +1,9 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.IOException;
+
+public class Subprocess {
+ public Process execute(String[] command) throws IOException {
+ return Runtime.getRuntime().exec(command);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/SyncTransport.java b/app/src/main/java/com/xypower/wpywapp/jadb/SyncTransport.java
new file mode 100644
index 0000000..f0e5186
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/SyncTransport.java
@@ -0,0 +1,115 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Created by vidstige on 2014-03-19.
+ */
+public class SyncTransport {
+
+ private final DataOutput output;
+ private final DataInput input;
+
+ public SyncTransport(DataOutput outputStream, DataInput inputStream) {
+ output = outputStream;
+ input = inputStream;
+ }
+
+ public void send(String syncCommand, String name) throws IOException {
+ if (syncCommand.length() != 4) throw new IllegalArgumentException("sync commands must have length 4");
+ output.writeBytes(syncCommand);
+ byte[] data = name.getBytes(StandardCharsets.UTF_8);
+ output.writeInt(Integer.reverseBytes(data.length));
+ output.write(data);
+ }
+
+ public void sendStatus(String statusCode, int length) throws IOException {
+ output.writeBytes(statusCode);
+ output.writeInt(Integer.reverseBytes(length));
+ }
+
+ public void verifyStatus() throws IOException, JadbException {
+ String status = readString(4);
+ int length = readInt();
+ if ("FAIL".equals(status)) {
+ String error = readString(length);
+ throw new JadbException(error);
+ }
+ if (!"OKAY".equals(status)) {
+ throw new JadbException("Unknown error: " + status);
+ }
+ }
+
+ private int readInt() throws IOException {
+ return Integer.reverseBytes(input.readInt());
+ }
+
+ private String readString(int length) throws IOException {
+ byte[] buffer = new byte[length];
+ input.readFully(buffer);
+ return new String(buffer, StandardCharsets.UTF_8);
+ }
+
+ public void sendDirectoryEntry(RemoteFile file) throws IOException {
+ output.writeBytes("DENT");
+ output.writeInt(Integer.reverseBytes(0666 | (file.isDirectory() ? (1 << 14) : 0)));
+ output.writeInt(Integer.reverseBytes(file.getSize()));
+ output.writeInt(Integer.reverseBytes(file.getLastModified()));
+ byte[] pathChars = file.getPath().getBytes(StandardCharsets.UTF_8);
+ output.writeInt(Integer.reverseBytes(pathChars.length));
+ output.write(pathChars);
+ }
+
+ public void sendDirectoryEntryDone() throws IOException {
+ output.writeBytes("DONE");
+ output.writeBytes("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); // equivalent to the length of a "normal" dent
+ }
+
+ public RemoteFileRecord readDirectoryEntry() throws IOException {
+ String id = readString(4);
+ int mode = readInt();
+ int size = readInt();
+ int time = readInt();
+ int nameLength = readInt();
+ String name = readString(nameLength);
+
+ if (!"DENT".equals(id)) return RemoteFileRecord.DONE;
+ return new RemoteFileRecord(name, mode, size, time);
+ }
+
+ private void sendChunk(byte[] buffer, int offset, int length) throws IOException {
+ output.writeBytes("DATA");
+ output.writeInt(Integer.reverseBytes(length));
+ output.write(buffer, offset, length);
+ }
+
+ private int readChunk(byte[] buffer) throws IOException, JadbException {
+ String id = readString(4);
+ int n = readInt();
+ if ("FAIL".equals(id)) {
+ throw new JadbException(readString(n));
+ }
+ if (!"DATA".equals(id)) return -1;
+ input.readFully(buffer, 0, n);
+ return n;
+ }
+
+ public void sendStream(InputStream in) throws IOException {
+ byte[] buffer = new byte[1024 * 64];
+ int n = in.read(buffer);
+ while (n != -1) {
+ sendChunk(buffer, 0, n);
+ n = in.read(buffer);
+ }
+ }
+
+ public void readChunksTo(OutputStream stream) throws IOException, JadbException {
+ byte[] buffer = new byte[1024 * 64];
+ int n = readChunk(buffer);
+ while (n != -1) {
+ stream.write(buffer, 0, n);
+ n = readChunk(buffer);
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/Transport.java b/app/src/main/java/com/xypower/wpywapp/jadb/Transport.java
new file mode 100644
index 0000000..52b97b4
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/Transport.java
@@ -0,0 +1,75 @@
+package com.xypower.wpywapp.jadb;
+
+import java.io.*;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+class Transport implements Closeable {
+
+ private final OutputStream outputStream;
+ private final InputStream inputStream;
+ private final DataInputStream dataInput;
+ private final DataOutputStream dataOutput;
+
+ private Transport(OutputStream outputStream, InputStream inputStream) {
+ this.outputStream = outputStream;
+ this.inputStream = inputStream;
+ this.dataInput = new DataInputStream(inputStream);
+ this.dataOutput = new DataOutputStream(outputStream);
+ }
+
+ public Transport(Socket socket) throws IOException {
+ this(socket.getOutputStream(), socket.getInputStream());
+ }
+
+ public String readString() throws IOException {
+ String encodedLength = readString(4);
+ int length = Integer.parseInt(encodedLength, 16);
+ return readString(length);
+ }
+
+ public void readResponseTo(OutputStream output) throws IOException {
+ Stream.copy(inputStream, output);
+ }
+
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+
+ public void verifyResponse() throws IOException, JadbException {
+ String response = readString(4);
+ if (!"OKAY".equals(response)) {
+ String error = readString();
+ throw new JadbException("command failed: " + error);
+ }
+ }
+
+ public String readString(int length) throws IOException {
+ byte[] responseBuffer = new byte[length];
+ dataInput.readFully(responseBuffer);
+ return new String(responseBuffer, StandardCharsets.UTF_8);
+ }
+
+ private String getCommandLength(String command) {
+ return String.format("%04x", command.getBytes().length);
+ }
+
+ public void send(String command) throws IOException {
+ OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
+ writer.write(getCommandLength(command));
+ writer.write(command);
+ writer.flush();
+ }
+
+ public SyncTransport startSync() throws IOException, JadbException {
+ send("sync:");
+ verifyResponse();
+ return new SyncTransport(dataOutput, dataInput);
+ }
+
+ @Override
+ public void close() throws IOException {
+ dataInput.close();
+ dataOutput.close();
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/managers/Bash.java b/app/src/main/java/com/xypower/wpywapp/jadb/managers/Bash.java
new file mode 100644
index 0000000..8ecab80
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/managers/Bash.java
@@ -0,0 +1,11 @@
+package com.xypower.wpywapp.jadb.managers;
+
+public class Bash {
+ private Bash() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ public static String quote(String s) {
+ return "'" + s.replace("'", "'\\''") + "'";
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/managers/Package.java b/app/src/main/java/com/xypower/wpywapp/jadb/managers/Package.java
new file mode 100644
index 0000000..8a1696d
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/managers/Package.java
@@ -0,0 +1,26 @@
+package com.xypower.wpywapp.jadb.managers;
+
+/**
+ * Android package
+ */
+public class Package {
+ private final String name;
+
+ public Package(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() { return name; }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Package)) return false;
+ Package that = (Package) o;
+ return name.equals(that.name);
+ }
+
+ @Override
+ public int hashCode() { return name.hashCode(); }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/managers/PackageManager.java b/app/src/main/java/com/xypower/wpywapp/jadb/managers/PackageManager.java
new file mode 100644
index 0000000..b5c7aa3
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/managers/PackageManager.java
@@ -0,0 +1,144 @@
+package com.xypower.wpywapp.jadb.managers;
+
+
+
+import com.xypower.wpywapp.jadb.JadbDevice;
+import com.xypower.wpywapp.jadb.JadbException;
+import com.xypower.wpywapp.jadb.RemoteFile;
+import com.xypower.wpywapp.jadb.Stream;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Java interface to package manager. Launches package manager through jadb
+ */
+public class PackageManager {
+ private final JadbDevice device;
+
+ public PackageManager(JadbDevice device) {
+ this.device = device;
+ }
+
+ public List getPackages() throws IOException, JadbException {
+ try (BufferedReader input = new BufferedReader(new InputStreamReader(device.executeShell("pm", "list", "packages"), StandardCharsets.UTF_8))) {
+ ArrayList result = new ArrayList<>();
+ String line;
+ while ((line = input.readLine()) != null) {
+ final String prefix = "package:";
+ if (line.startsWith(prefix)) {
+ result.add(new Package(line.substring(prefix.length())));
+ }
+ }
+ return result;
+ }
+ }
+
+ private String getErrorMessage(String operation, String target, String errorMessage) {
+ return "Could not " + operation + " " + target + ": " + errorMessage;
+ }
+
+ private void verifyOperation(String operation, String target, String result) throws JadbException {
+ if (!result.contains("Success")) throw new JadbException(getErrorMessage(operation, target, result));
+ }
+
+ private void remove(RemoteFile file) throws IOException, JadbException {
+ InputStream s = device.executeShell("rm", "-f", file.getPath());
+ Stream.readAll(s, StandardCharsets.UTF_8);
+ }
+
+ private void install(File apkFile, List extraArguments) throws IOException, JadbException {
+ RemoteFile remote = new RemoteFile("/data/local/tmp/" + apkFile.getName());
+ device.push(apkFile, remote);
+ List arguments = new ArrayList<>();
+ arguments.add("install");
+ arguments.addAll(extraArguments);
+ arguments.add(remote.getPath());
+ InputStream s = device.executeShell("pm", arguments.toArray(new String[0]));
+ String result = Stream.readAll(s, StandardCharsets.UTF_8);
+ remove(remote);
+ verifyOperation("install", apkFile.getName(), result);
+ }
+
+ public void install(File apkFile) throws IOException, JadbException {
+ install(apkFile, new ArrayList(0));
+ }
+
+ public void installWithOptions(File apkFile, List extends InstallOption> options) throws IOException, JadbException {
+ List optionsAsStr = new ArrayList<>(options.size());
+
+ for(InstallOption installOption : options) {
+ optionsAsStr.add(installOption.getStringRepresentation());
+ }
+ install(apkFile, optionsAsStr);
+ }
+
+ public void forceInstall(File apkFile) throws IOException, JadbException {
+ installWithOptions(apkFile, Collections.singletonList(REINSTALL_KEEPING_DATA));
+ }
+
+ public void uninstall(java.lang.Package name) throws IOException, JadbException {
+ InputStream s = device.executeShell("pm", "uninstall", name.toString());
+ String result = Stream.readAll(s, StandardCharsets.UTF_8);
+ verifyOperation("uninstall", name.toString(), result);
+ }
+
+ public void launch(java.lang.Package name) throws IOException, JadbException {
+ InputStream s = device.executeShell("monkey", "-p", name.toString(), "-c", "android.intent.category.LAUNCHER", "1");
+ s.close();
+ }
+
+ //
+ public static class InstallOption {
+ private final StringBuilder stringBuilder = new StringBuilder();
+
+ InstallOption(String ... varargs) {
+ String suffix = "";
+ for(String str: varargs) {
+ stringBuilder.append(suffix).append(str);
+ suffix = " ";
+ }
+ }
+
+ private String getStringRepresentation() {
+ return stringBuilder.toString();
+ }
+ }
+
+ public static final InstallOption WITH_FORWARD_LOCK = new InstallOption("-l");
+
+ public static final InstallOption REINSTALL_KEEPING_DATA =
+ new InstallOption("-r");
+
+ public static final InstallOption ALLOW_TEST_APK =
+ new InstallOption("-t");
+
+ @SuppressWarnings("squid:S00100")
+ public static InstallOption WITH_INSTALLER_PACKAGE_NAME(String name)
+ {
+ return new InstallOption("-t", name);
+ }
+
+ @SuppressWarnings("squid:S00100")
+ public static InstallOption ON_SHARED_MASS_STORAGE(String name) {
+ return new InstallOption("-s", name);
+ }
+
+ @SuppressWarnings("squid:S00100")
+ public static InstallOption ON_INTERNAL_SYSTEM_MEMORY(String name) {
+ return new InstallOption("-f", name);
+ }
+
+ public static final InstallOption ALLOW_VERSION_DOWNGRADE =
+ new InstallOption("-d");
+
+ /**
+ * This option is supported only from Android 6.X+
+ */
+ public static final InstallOption GRANT_ALL_PERMISSIONS = new InstallOption("-g");
+
+ //
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/managers/PropertyManager.java b/app/src/main/java/com/xypower/wpywapp/jadb/managers/PropertyManager.java
new file mode 100644
index 0000000..8767bd0
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/managers/PropertyManager.java
@@ -0,0 +1,57 @@
+package com.xypower.wpywapp.jadb.managers;
+
+
+
+import com.xypower.wpywapp.jadb.JadbDevice;
+import com.xypower.wpywapp.jadb.JadbException;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A class which works with properties, uses getprop and setprop methods of android shell
+ */
+public class PropertyManager {
+ private final Pattern pattern = Pattern.compile("^\\[([a-zA-Z0-9_.-]*)]:.\\[([^\\[\\]]*)]");
+ private final JadbDevice device;
+
+ public PropertyManager(JadbDevice device) {
+ this.device = device;
+ }
+
+ public Map getprop() throws IOException, JadbException {
+ try (BufferedReader bufferedReader =
+ new BufferedReader(new InputStreamReader(device.executeShell("getprop"), StandardCharsets.UTF_8))) {
+ return parseProp(bufferedReader);
+ }
+ }
+
+ private Map parseProp(BufferedReader bufferedReader) throws IOException {
+ HashMap result = new HashMap<>();
+
+ String line;
+ Matcher matcher = pattern.matcher("");
+
+ while ((line = bufferedReader.readLine()) != null) {
+ matcher.reset(line);
+
+ if (matcher.find()) {
+ if (matcher.groupCount() < 2) {
+ System.err.println("Property line: " + line + " does not match pattern. Ignoring");
+ continue;
+ }
+ String key = matcher.group(1);
+ String value = matcher.group(2);
+ result.put(key, value);
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbDeviceResponder.java b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbDeviceResponder.java
new file mode 100644
index 0000000..592cb1c
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbDeviceResponder.java
@@ -0,0 +1,25 @@
+package com.xypower.wpywapp.jadb.server;
+
+
+
+import com.xypower.wpywapp.jadb.JadbException;
+import com.xypower.wpywapp.jadb.RemoteFile;
+
+import java.io.*;
+import java.util.List;
+
+/**
+ * Created by vidstige on 20/03/14.
+ */
+public interface AdbDeviceResponder {
+ String getSerial();
+ String getType();
+
+ void filePushed(RemoteFile path, int mode, ByteArrayOutputStream buffer) throws JadbException;
+ void filePulled(RemoteFile path, ByteArrayOutputStream buffer) throws JadbException, IOException;
+
+ void shell(String command, DataOutputStream stdout, DataInput stdin) throws IOException;
+ void enableIpCommand(String ip, DataOutputStream outputStream) throws IOException;
+
+ List list(String path) throws IOException;
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbProtocolHandler.java b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbProtocolHandler.java
new file mode 100644
index 0000000..631eae5
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbProtocolHandler.java
@@ -0,0 +1,248 @@
+package com.xypower.wpywapp.jadb.server;
+
+
+
+import com.xypower.wpywapp.jadb.JadbException;
+import com.xypower.wpywapp.jadb.RemoteFile;
+import com.xypower.wpywapp.jadb.SyncTransport;
+
+import java.io.*;
+import java.net.ProtocolException;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+class AdbProtocolHandler implements Runnable {
+ private final Socket socket;
+ private final AdbResponder responder;
+ private AdbDeviceResponder selected;
+
+ public AdbProtocolHandler(Socket socket, AdbResponder responder) {
+ this.socket = socket;
+ this.responder = responder;
+ }
+
+ private AdbDeviceResponder findDevice(String serial) throws ProtocolException {
+ for (AdbDeviceResponder d : responder.getDevices()) {
+ if (d.getSerial().equals(serial)) return d;
+ }
+ throw new ProtocolException("'" + serial + "' not connected");
+ }
+
+ @Override
+ public void run() {
+ try {
+ runServer();
+ } catch (IOException e) {
+ if (e.getMessage() != null) // thrown when exiting for some reason
+ System.out.println("IO Error: " + e.getMessage());
+ }
+ }
+
+ private void runServer() throws IOException {
+ try (
+ DataInputStream input = new DataInputStream(socket.getInputStream());
+ DataOutputStream output = new DataOutputStream(socket.getOutputStream())
+ ) {
+ //noinspection StatementWithEmptyBody
+ while (processCommand(input, output)) {
+ // nothing to do here
+ }
+ }
+ }
+
+ private boolean processCommand(DataInput input, DataOutputStream output) throws IOException {
+ String command = readCommand(input);
+ responder.onCommand(command);
+
+ try {
+ if ("host:version".equals(command)) {
+ hostVersion(output);
+ } else if ("host:transport-any".equals(command)) {
+ hostTransportAny(output);
+ } else if ("host:devices".equals(command)) {
+ hostDevices(output);
+ } else if (command.startsWith("host:transport:")) {
+ hostTransport(output, command);
+ } else if ("sync:".equals(command)) {
+ sync(output, input);
+ } else if (command.startsWith("shell:")) {
+ shell(input, output, command);
+ return false;
+ } else if ("host:get-state".equals(command)) {
+ hostGetState(output);
+ } else if (command.startsWith("host-serial:")) {
+ hostSerial(output, command);
+ } else if (command.startsWith("tcpip:")) {
+ handleTcpip(output, command);
+ } else {
+ throw new ProtocolException("Unknown command: " + command);
+ }
+ } catch (ProtocolException e) {
+ output.writeBytes("FAIL");
+ send(output, e.getMessage());
+ }
+ output.flush();
+ return true;
+ }
+
+ private void handleTcpip(DataOutputStream output, String command) throws IOException {
+ output.writeBytes("OKAY");
+ selected.enableIpCommand(command.substring("tcpip:".length()), output);
+ }
+
+ private void hostSerial(DataOutput output, String command) throws IOException {
+ String[] strs = command.split(":",0);
+ if (strs.length != 3) {
+ throw new ProtocolException("Invalid command: " + command);
+ }
+
+ String serial = strs[1];
+ boolean found = false;
+ output.writeBytes("OKAY");
+ for (AdbDeviceResponder d : responder.getDevices()) {
+ if (d.getSerial().equals(serial)) {
+ send(output, d.getType());
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ send(output, "unknown");
+ }
+ }
+
+ private void hostGetState(DataOutput output) throws IOException {
+ // TODO: Check so that exactly one device is selected.
+ AdbDeviceResponder device = responder.getDevices().get(0);
+ output.writeBytes("OKAY");
+ send(output, device.getType());
+ }
+
+ private void shell(DataInput input, DataOutputStream output, String command) throws IOException {
+ String shellCommand = command.substring("shell:".length());
+ output.writeBytes("OKAY");
+ shell(shellCommand, output, input);
+ }
+
+ private void hostTransport(DataOutput output, String command) throws IOException {
+ String serial = command.substring("host:transport:".length());
+ selected = findDevice(serial);
+ output.writeBytes("OKAY");
+ }
+
+ private void hostDevices(DataOutput output) throws IOException {
+ ByteArrayOutputStream tmp = new ByteArrayOutputStream();
+ DataOutputStream writer = new DataOutputStream(tmp);
+ for (AdbDeviceResponder d : responder.getDevices()) {
+ writer.writeBytes(d.getSerial() + "\t" + d.getType() + "\n");
+ }
+ output.writeBytes("OKAY");
+ send(output, new String(tmp.toByteArray(), StandardCharsets.UTF_8));
+ }
+
+ private void hostTransportAny(DataOutput output) throws IOException {
+ // TODO: Check so that exactly one device is selected.
+ selected = responder.getDevices().get(0);
+ output.writeBytes("OKAY");
+ }
+
+ private void hostVersion(DataOutput output) throws IOException {
+ output.writeBytes("OKAY");
+ send(output, String.format("%04x", responder.getVersion()));
+ }
+
+ private void shell(String command, DataOutputStream stdout, DataInput stdin) throws IOException {
+ selected.shell(command, stdout, stdin);
+ }
+
+ private int readInt(DataInput input) throws IOException {
+ return Integer.reverseBytes(input.readInt());
+ }
+
+ private int readHexInt(DataInput input) throws IOException {
+ return Integer.parseInt(readString(input, 4), 16);
+ }
+
+ private String readString(DataInput input, int length) throws IOException {
+ byte[] responseBuffer = new byte[length];
+ input.readFully(responseBuffer);
+ return new String(responseBuffer, StandardCharsets.UTF_8);
+ }
+
+ private String readCommand(DataInput input) throws IOException {
+ int length = readHexInt(input);
+ return readString(input, length);
+ }
+
+ private void sync(DataOutput output, DataInput input) throws IOException {
+ output.writeBytes("OKAY");
+ try {
+ String id = readString(input, 4);
+ int length = readInt(input);
+ switch (id) {
+ case "SEND":
+ syncSend(output, input, length);
+ break;
+ case "RECV":
+ syncRecv(output, input, length);
+ break;
+ case "LIST":
+ syncList(output, input, length);
+ break;
+ default:
+ throw new JadbException("Unknown sync id " + id);
+ }
+ } catch (JadbException e) { // sync response with a different type of fail message
+ SyncTransport sync = getSyncTransport(output, input);
+ sync.send("FAIL", e.getMessage());
+ }
+ }
+
+ private void syncRecv(DataOutput output, DataInput input, int length) throws IOException, JadbException {
+ String remotePath = readString(input, length);
+ SyncTransport transport = getSyncTransport(output, input);
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ selected.filePulled(new RemoteFile(remotePath), buffer);
+ transport.sendStream(new ByteArrayInputStream(buffer.toByteArray()));
+ transport.sendStatus("DONE", 0); // ignored
+ }
+
+ private void syncSend(DataOutput output, DataInput input, int length) throws IOException, JadbException {
+ String remotePath = readString(input, length);
+ int idx = remotePath.lastIndexOf(',');
+ String path = remotePath;
+ int mode = 0666;
+ if (idx > 0) {
+ path = remotePath.substring(0, idx);
+ mode = Integer.parseInt(remotePath.substring(idx + 1));
+ }
+ SyncTransport transport = getSyncTransport(output, input);
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ transport.readChunksTo(buffer);
+ selected.filePushed(new RemoteFile(path), mode, buffer);
+ transport.sendStatus("OKAY", 0); // 0 = ignored
+ }
+
+ private void syncList(DataOutput output, DataInput input, int length) throws IOException, JadbException {
+ String remotePath = readString(input, length);
+ SyncTransport transport = getSyncTransport(output, input);
+ for (RemoteFile file : selected.list(remotePath)) {
+ transport.sendDirectoryEntry(file);
+ }
+ transport.sendDirectoryEntryDone();
+ }
+
+ private String getCommandLength(String command) {
+ return String.format("%04x", command.length());
+ }
+
+ private void send(DataOutput writer, String response) throws IOException {
+ writer.writeBytes(getCommandLength(response));
+ writer.writeBytes(response);
+ }
+
+ private SyncTransport getSyncTransport(DataOutput output, DataInput input) {
+ return new SyncTransport(output, input);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbResponder.java b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbResponder.java
new file mode 100644
index 0000000..1956843
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbResponder.java
@@ -0,0 +1,14 @@
+package com.xypower.wpywapp.jadb.server;
+
+import java.util.List;
+
+/**
+ * Created by vidstige on 20/03/14.
+ */
+public interface AdbResponder {
+ void onCommand(String command);
+
+ int getVersion();
+
+ List getDevices();
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbServer.java b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbServer.java
new file mode 100644
index 0000000..13beae8
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbServer.java
@@ -0,0 +1,26 @@
+package com.xypower.wpywapp.jadb.server;
+
+import java.net.Socket;
+
+/**
+ * Created by vidstige on 2014-03-20
+ */
+public class AdbServer extends SocketServer {
+
+ public static final int DEFAULT_PORT = 15037;
+ private final AdbResponder responder;
+
+ public AdbServer(AdbResponder responder) {
+ this(responder, DEFAULT_PORT);
+ }
+
+ public AdbServer(AdbResponder responder, int port) {
+ super(port);
+ this.responder = responder;
+ }
+
+ @Override
+ public Runnable createResponder(Socket socket) {
+ return new AdbProtocolHandler(socket, responder);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/server/SocketServer.java b/app/src/main/java/com/xypower/wpywapp/jadb/server/SocketServer.java
new file mode 100644
index 0000000..c4a24e9
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/jadb/server/SocketServer.java
@@ -0,0 +1,71 @@
+package com.xypower.wpywapp.jadb.server;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+// >set ANDROID_ADB_SERVER_PORT=15037
+public abstract class SocketServer implements Runnable {
+
+ private final int port;
+ private ServerSocket socket;
+ private Thread thread;
+
+ private boolean isStarted = false;
+ private final Object lockObject = new Object();
+
+ protected SocketServer(int port) {
+ this.port = port;
+ }
+
+ public void start() throws InterruptedException {
+ thread = new Thread(this, "Fake Adb Server");
+ thread.setDaemon(true);
+ thread.start();
+ waitForServer();
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ @SuppressWarnings("squid:S2189") // server is stopped by closing SocketServer
+ @Override
+ public void run() {
+ try {
+ socket = new ServerSocket(port);
+ socket.setReuseAddress(true);
+
+ serverReady();
+
+ while (true) {
+ Socket c = socket.accept();
+ createResponder(c).run();
+ }
+ } catch (IOException e) {
+ // Empty on purpose
+ }
+ }
+
+ private void serverReady() {
+ synchronized (lockObject) {
+ isStarted = true;
+ lockObject.notifyAll();
+ }
+ }
+
+ private void waitForServer() throws InterruptedException {
+ synchronized (lockObject) {
+ while (!isStarted) {
+ lockObject.wait();
+ }
+ }
+ }
+
+ protected abstract Runnable createResponder(Socket socket);
+
+ public void stop() throws IOException, InterruptedException {
+ socket.close();
+ thread.join();
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/.gitignore b/app/src/main/java/com/xypower/wpywapp/libadb/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/build.gradle b/app/src/main/java/com/xypower/wpywapp/libadb/build.gradle
new file mode 100644
index 0000000..4fd917d
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/build.gradle
@@ -0,0 +1,60 @@
+package com.xypower.wpywapp.libadb
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+plugins {
+ id 'com.android.library'
+ id 'maven-publish'
+}
+
+group = 'io.github.muntashirakon'
+version = '3.0.0'
+
+android {
+ compileSdk 34
+ namespace "io.github.muntashirakon.adb"
+
+ defaultConfig {
+ minSdk 1
+ targetSdk 34
+ aarMetadata {
+ minCompileSdk = 1
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ publishing {
+ singleVariant("release") {
+ withSourcesJar()
+ withJavadocJar()
+ }
+ }
+}
+
+publishing {
+ publications {
+ release(MavenPublication) {
+ artifactId = 'libadb'
+ afterEvaluate {
+ from components.release
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation "androidx.annotation:annotation:1.7.1"
+ implementation 'org.bouncycastle:bcprov-jdk15to18:1.78'
+ implementation 'com.github.MuntashirAkon.spake2-java:android:2.0.0'
+
+ testImplementation 'junit:junit:4.13.2'
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/AndroidManifest.xml b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c9d05a3
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AbsAdbConnectionManager.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AbsAdbConnectionManager.java
new file mode 100644
index 0000000..d35030a
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AbsAdbConnectionManager.java
@@ -0,0 +1,509 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.WorkerThread;
+
+import com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.android.AdbMdns;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.security.auth.DestroyFailedException;
+
+
+@SuppressWarnings("unused")
+public abstract class AbsAdbConnectionManager implements Closeable {
+ private final Object mLock = new Object();
+ @Nullable
+ private AdbConnection mAdbConnection;
+ private String mHostAddress = "127.0.0.1";
+ private int mApi = Build.VERSION_CODES.P;
+ private long mTimeout = Long.MAX_VALUE;
+ private TimeUnit mTimeoutUnit = TimeUnit.MILLISECONDS;
+ private boolean mThrowOnUnauthorised = false;
+
+ /**
+ * Return generated/stored private key.
+ */
+ @NonNull
+ protected abstract PrivateKey getPrivateKey();
+
+ /**
+ * Return public key wrapped around a certificate.
+ */
+ @NonNull
+ protected abstract Certificate getCertificate();
+
+ /**
+ * Return a name for the device. This can be the app label, hostname or user@hostname.
+ */
+ @NonNull
+ protected abstract String getDeviceName();
+
+ /**
+ * Set host address for this connection. On the same device, this should be {@code 127.0.0.1}.
+ */
+ @CallSuper
+ public void setHostAddress(@NonNull String hostAddress) {
+ mHostAddress = Objects.requireNonNull(hostAddress);
+ }
+
+ /**
+ * Get host address for this connection. Default value is {@code 127.0.0.1}.
+ */
+ @NonNull
+ public String getHostAddress() {
+ return mHostAddress;
+ }
+
+ /**
+ * Set Android API (i.e. SDK) version for this connection. If the daemon and the client are located in the same
+ * directory, the value should be {@link Build.VERSION#SDK_INT} in order to improve performance as well as security.
+ *
+ * @param api The API version, default is {@link Build.VERSION_CODES#BASE}.
+ */
+ public void setApi(int api) {
+ this.mApi = api;
+ }
+
+ /**
+ * Get Android API (i.e. SDK) version for this connection. Default value is {@link Build.VERSION_CODES#BASE}.
+ */
+ public int getApi() {
+ return mApi;
+ }
+
+ /**
+ * Set time to wait for the connection to be made.
+ *
+ * @param timeout Timeout value
+ * @param unit Timeout unit
+ */
+ @CallSuper
+ public void setTimeout(long timeout, TimeUnit unit) {
+ mTimeout = timeout;
+ mTimeoutUnit = unit;
+ }
+
+ /**
+ * Get time to wait for the connection to be made. If not set using {@link #setTimeout(long, TimeUnit)}, the default
+ * timeout is {@link Long#MAX_VALUE} milliseconds.
+ *
+ * @return Timeout in milliseconds
+ */
+ public long getTimeout() {
+ return mTimeoutUnit.toMillis(mTimeout);
+ }
+
+ /**
+ * Get the unit for the timeout. If not set using {@link #setTimeout(long, TimeUnit)}, the default timeout unit is
+ * {@link TimeUnit#MILLISECONDS}.
+ */
+ @NonNull
+ public TimeUnit getTimeoutUnit() {
+ return mTimeoutUnit;
+ }
+
+ /**
+ * Set whether to throw {@link AdbAuthenticationFailedException} if the daemon rejects the first authentication
+ * attempt.
+ *
+ * @param throwOnUnauthorised {@code true} to throw {@link AdbAuthenticationFailedException} or {@code false}
+ * otherwise.
+ */
+ @CallSuper
+ public void setThrowOnUnauthorised(boolean throwOnUnauthorised) {
+ mThrowOnUnauthorised = throwOnUnauthorised;
+ }
+
+ /**
+ * Get whether to throw {@link AdbAuthenticationFailedException} if the daemon rejects the first authentication
+ * attempt.
+ *
+ * @return {@code true} if the system is configured to throw {@link AdbAuthenticationFailedException} or
+ * {@code false} otherwise. The default value is {@code false}.
+ */
+ public boolean isThrowOnUnauthorised() {
+ return mThrowOnUnauthorised;
+ }
+
+ /**
+ * Get the {@link AdbConnection} backed by this object.
+ *
+ * @return Underlying {@link AdbConnection}, or {@code null} if the connection hasn't been made yet.
+ */
+ @CallSuper
+ @Nullable
+ public AdbConnection getAdbConnection() {
+ synchronized (mLock) {
+ return mAdbConnection;
+ }
+ }
+
+ /**
+ * Check if it is connected to an ADB daemon.
+ *
+ * @return {@code true} if connected, {@code false} otherwise.
+ */
+ public boolean isConnected() {
+ synchronized (mLock) {
+ return mAdbConnection != null && mAdbConnection.isConnected() && mAdbConnection.isConnectionEstablished();
+ }
+ }
+
+ /**
+ * Attempt to connect to ADB by performing an automatic network discovery of TLS host and port. Host address set by
+ * {@link #setHostAddress(String)} is ignored.
+ *
+ * @param context Application context
+ * @param timeoutMillis Amount of time spent in searching for a host and a port.
+ * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection
+ * attempt is unsuccessful, or it has already been made.
+ * @throws IOException If the socket connection could not be made.
+ * @throws InterruptedException If timeout has reached.
+ * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB
+ * daemon has rejected the first authentication attempt, which indicates
+ * that the daemon has not saved the public key from a previous connection.
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ @WorkerThread
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+ public boolean connectTls(@NonNull Context context, long timeoutMillis)
+ throws IOException, InterruptedException, AdbPairingRequiredException {
+ return autoConnect(context, AdbMdns.SERVICE_TYPE_TLS_CONNECT, timeoutMillis);
+ }
+
+ /**
+ * Attempt to connect to ADB by performing an automatic network discovery of TCP host and port. Host address set by
+ * {@link #setHostAddress(String)} is ignored.
+ *
+ * @param context Application context
+ * @param timeoutMillis Amount of time spent in searching for a host and a port.
+ * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection
+ * attempt is unsuccessful, or it has already been made.
+ * @throws IOException If the socket connection could not be made.
+ * @throws InterruptedException If timeout has reached.
+ * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB
+ * daemon has rejected the first authentication attempt, which indicates
+ * that the daemon has not saved the public key from a previous connection.
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ @WorkerThread
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+ public boolean connectTcp(@NonNull Context context, long timeoutMillis)
+ throws IOException, InterruptedException, AdbPairingRequiredException {
+ return autoConnect(context, AdbMdns.SERVICE_TYPE_ADB, timeoutMillis);
+ }
+
+ /**
+ * Attempt to connect to ADB by performing an automatic network discovery of host and port. Host address set by
+ * {@link #setHostAddress(String)} is ignored.
+ *
+ * @param context Application context
+ * @param timeoutMillis Amount of time spent in searching for a host and a port.
+ * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection
+ * attempt is unsuccessful, or it has already been made.
+ * @throws IOException If the socket connection could not be made.
+ * @throws InterruptedException If timeout has reached.
+ * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB
+ * daemon has rejected the first authentication attempt, which indicates
+ * that the daemon has not saved the public key from a previous connection.
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ @WorkerThread
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+ public boolean autoConnect(@NonNull Context context, long timeoutMillis)
+ throws IOException, InterruptedException, AdbPairingRequiredException {
+ synchronized (mLock) {
+ AtomicInteger atomicPort = new AtomicInteger(-1);
+ AtomicReference atomicHostAddress = new AtomicReference<>(null);
+ CountDownLatch resolveHostAndPort = new CountDownLatch(1);
+
+ AdbMdns adbMdnsTcp = new AdbMdns(context, AdbMdns.SERVICE_TYPE_ADB, (hostAddress, port) -> {
+ if (hostAddress != null) {
+ atomicHostAddress.set(hostAddress.getHostAddress());
+ atomicPort.set(port);
+ }
+ resolveHostAndPort.countDown();
+ });
+ adbMdnsTcp.start();
+
+ AdbMdns adbMdnsTls = new AdbMdns(context, AdbMdns.SERVICE_TYPE_TLS_CONNECT, (hostAddress, port) -> {
+ if (hostAddress != null) {
+ atomicHostAddress.set(hostAddress.getHostAddress());
+ atomicPort.set(port);
+ }
+ resolveHostAndPort.countDown();
+ });
+ adbMdnsTls.start();
+
+ try {
+ if (!resolveHostAndPort.await(timeoutMillis, TimeUnit.MILLISECONDS)) {
+ throw new InterruptedException("Timed out while trying to find a valid host address and port");
+ }
+ } finally {
+ adbMdnsTcp.stop();
+ adbMdnsTls.stop();
+ }
+
+ String host = atomicHostAddress.get();
+ int port = atomicPort.get();
+
+ if (host == null || port == -1) {
+ throw new IOException("Could not find any valid host address or port");
+ }
+
+ mHostAddress = host;
+ mAdbConnection = new AdbConnection.Builder(host, port)
+ .setApi(mApi)
+ .setKeyPair(getAdbKeyPair())
+ .setDeviceName(Objects.requireNonNull(getDeviceName()))
+ .build();
+ return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised);
+ }
+ }
+
+ @WorkerThread
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+ private boolean autoConnect(@NonNull Context context, @AdbMdns.ServiceType @NonNull String serviceType, long timeoutMillis)
+ throws IOException, InterruptedException, AdbPairingRequiredException {
+ synchronized (mLock) {
+ AtomicInteger atomicPort = new AtomicInteger(-1);
+ AtomicReference atomicHostAddress = new AtomicReference<>(null);
+ CountDownLatch resolveHostAndPort = new CountDownLatch(1);
+
+ AdbMdns adbMdns = new AdbMdns(context, serviceType, (hostAddress, port) -> {
+ if (hostAddress != null) {
+ atomicHostAddress.set(hostAddress.getHostAddress());
+ atomicPort.set(port);
+ }
+ resolveHostAndPort.countDown();
+ });
+ adbMdns.start();
+
+ try {
+ if (!resolveHostAndPort.await(timeoutMillis, TimeUnit.MILLISECONDS)) {
+ throw new InterruptedException("Timed out while trying to find a valid host address and port");
+ }
+ } finally {
+ adbMdns.stop();
+ }
+
+ String host = atomicHostAddress.get();
+ int port = atomicPort.get();
+
+ if (host == null || port == -1) {
+ throw new IOException("Could not find any valid host address or port");
+ }
+
+ mHostAddress = host;
+ mAdbConnection = new AdbConnection.Builder(host, port)
+ .setApi(mApi)
+ .setKeyPair(getAdbKeyPair())
+ .setDeviceName(Objects.requireNonNull(getDeviceName()))
+ .build();
+ return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised);
+ }
+ }
+
+ /**
+ * Attempt to connect to ADB given a port number. Host address is set via {@link #setHostAddress(String)}.
+ *
+ * @param port Port number
+ * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection
+ * attempt is unsuccessful, or it has already been made.
+ * @throws IOException If the socket connection could not be made.
+ * @throws InterruptedException If timeout has reached.
+ * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB
+ * daemon has rejected the first authentication attempt, which indicates
+ * that the daemon has not saved the public key from a previous connection.
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ @WorkerThread
+ public boolean connect(int port) throws IOException, InterruptedException, AdbPairingRequiredException {
+ synchronized (mLock) {
+ if (isConnected()) {
+ return false;
+ }
+ mAdbConnection = new AdbConnection.Builder(mHostAddress, port)
+ .setApi(mApi)
+ .setKeyPair(getAdbKeyPair())
+ .setDeviceName(Objects.requireNonNull(getDeviceName()))
+ .build();
+ return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised);
+ }
+ }
+
+ /**
+ * Attempt to connect to ADB via a host address and a port number.
+ *
+ * @param host Host address to use instead of taking it from the {@link #getHostAddress()}
+ * @param port Port number
+ * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection
+ * attempt is unsuccessful, or it has already been made.
+ * @throws IOException If the socket connection could not be made.
+ * @throws InterruptedException If timeout has reached.
+ * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the
+ * ADB daemon has rejected the first authentication attempt, which
+ * indicates that the daemon has not saved the public key from a previous
+ * connection.
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ @WorkerThread
+ public boolean connect(@NonNull String host, int port)
+ throws IOException, InterruptedException, AdbPairingRequiredException {
+ synchronized (mLock) {
+ if (isConnected()) {
+ return false;
+ }
+ mHostAddress = host;
+ mAdbConnection = new AdbConnection.Builder(host, port)
+ .setApi(mApi)
+ .setKeyPair(getAdbKeyPair())
+ .setDeviceName(Objects.requireNonNull(getDeviceName()))
+ .build();
+ return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised);
+ }
+ }
+
+ /**
+ * Disconnect the underlying {@link AdbConnection}.
+ *
+ * @throws IOException If the underlying socket fails to close
+ */
+ public void disconnect() throws IOException {
+ synchronized (mLock) {
+ if (mAdbConnection != null) {
+ mAdbConnection.close();
+ mAdbConnection = null;
+ }
+ }
+ }
+
+ /**
+ * Opens an {@link AdbStream} object corresponding to the specified destination.
+ * This routine will block until the connection completes.
+ *
+ * @param destination The destination to open on the target
+ * @return {@link AdbStream} object corresponding to the specified destination
+ * @throws IOException If the steam fails or no connection has been made
+ * @throws InterruptedException If the stream fails while sending the packet
+ * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8.
+ */
+ @WorkerThread
+ @NonNull
+ public AdbStream openStream(String destination) throws IOException, InterruptedException {
+ synchronized (mLock) {
+ if (mAdbConnection != null && mAdbConnection.isConnected()) {
+ try {
+ return mAdbConnection.open(destination);
+ } catch (AdbPairingRequiredException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ throw new IOException("Not connected to ADB.");
+ }
+ }
+
+ /**
+ * Opens an {@link AdbStream} object corresponding to the specified destination.
+ * This routine will block until the connection completes.
+ *
+ * @param service The service to open. One of the services under {@link LocalServices.Services}.
+ * @param args Additional arguments supported by the service (see the corresponding constant to learn more).
+ * @return AdbStream object corresponding to the specified destination
+ * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8
+ * @throws IOException If the stream fails while sending the packet
+ * @throws InterruptedException If we are unable to wait for the connection to finish
+ */
+ @NonNull
+ public AdbStream openStream(@LocalServices.Services int service, @NonNull String... args)
+ throws IOException, InterruptedException {
+ synchronized (mLock) {
+ if (mAdbConnection != null && mAdbConnection.isConnected()) {
+ try {
+ return mAdbConnection.open(service, args);
+ } catch (AdbPairingRequiredException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ throw new IOException("Not connected to ADB.");
+ }
+ }
+
+ /**
+ * Pair with an ADB daemon given port number and pairing code.
+ *
+ * @param port Port number
+ * @param pairingCode The six-digit pairing code as string
+ * @return {@code true} if the pairing is successful and {@code false} otherwise.
+ * @throws Exception If pairing failed for some reason.
+ */
+ @WorkerThread
+ @RequiresApi(Build.VERSION_CODES.GINGERBREAD)
+ public boolean pair(int port, @NonNull String pairingCode) throws Exception {
+ return pair(mHostAddress, port, pairingCode);
+ }
+
+ /**
+ * Pair with an ADB daemon given host address, port number and pairing code.
+ *
+ * @param host Host address to use instead of taking it from the {@link #getHostAddress()}
+ * @param port Port number
+ * @param pairingCode The six-digit pairing code as string
+ * @return {@code true} if the pairing is successful and {@code false} otherwise.
+ * @throws Exception If pairing failed for some reason.
+ */
+ @WorkerThread
+ @RequiresApi(Build.VERSION_CODES.GINGERBREAD)
+ public boolean pair(@NonNull String host, int port, @NonNull String pairingCode) throws Exception {
+ synchronized (mLock) {
+ KeyPair keyPair = getAdbKeyPair();
+ try (PairingConnectionCtx pairingClient = new PairingConnectionCtx(Objects.requireNonNull(host), port,
+ StringCompat.getBytes(Objects.requireNonNull(pairingCode), "UTF-8"), keyPair, getDeviceName())) {
+ // TODO: 5/12/21 Return true/false instead of only exceptions
+ pairingClient.start();
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Close the underlying {@link AdbConnection} and destroy the private key.
+ *
+ * @throws IOException If socket fails to close.
+ */
+ @Override
+ public void close() throws IOException {
+ try {
+ getPrivateKey().destroy();
+ } catch (DestroyFailedException | NoSuchMethodError e) {
+ e.printStackTrace();
+ }
+ if (mAdbConnection != null) {
+ mAdbConnection.close();
+ mAdbConnection = null;
+ }
+ }
+
+ @NonNull
+ private KeyPair getAdbKeyPair() {
+ return new KeyPair(Objects.requireNonNull(getPrivateKey()), Objects.requireNonNull(getCertificate()));
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbAuthenticationFailedException.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbAuthenticationFailedException.java
new file mode 100644
index 0000000..e769efc
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbAuthenticationFailedException.java
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0)
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+/**
+ * Thrown when the ADB daemon rejects our initial authentication attempt, which typically means that the peer has not
+ * previously saved our public key.
+ */
+// Copyright 2020 Sam Palmer
+public class AdbAuthenticationFailedException extends RuntimeException {
+ public AdbAuthenticationFailedException() {
+ super("Initial authentication attempt rejected by peer.");
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java
new file mode 100644
index 0000000..d987b51
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java
@@ -0,0 +1,749 @@
+// SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0)
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.ConnectException;
+import java.net.Socket;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.security.auth.DestroyFailedException;
+
+/**
+ * This class represents an ADB connection.
+ */
+// Copyright 2013 Cameron Gutman
+public class AdbConnection implements Closeable {
+ public static final String TAG = AdbConnection.class.getSimpleName();
+
+ /**
+ * The underlying socket that this class uses to communicate with the target device.
+ */
+ @NonNull
+ private final Socket mSocket;
+
+ @NonNull
+ private final String mHost;
+
+ private final int mPort;
+
+ private final int mApi;
+
+ /**
+ * The last allocated local stream ID. The ID chosen for the next stream will be this value + 1.
+ */
+ private int mLastLocalId;
+
+ /**
+ * The input stream that this class uses to read from the socket.
+ */
+ @GuardedBy("lock")
+ @NonNull
+ private final InputStream mPlainInputStream;
+
+ /**
+ * The output stream that this class uses to read from the socket.
+ */
+ @GuardedBy("lock")
+ @NonNull
+ private final OutputStream mPlainOutputStream;
+
+ /**
+ * The input stream that this class uses to read from the TLS socket.
+ */
+ @GuardedBy("lock")
+ @Nullable
+ private volatile InputStream mTlsInputStream;
+
+ /**
+ * The output stream that this class uses to read from the TLS socket.
+ */
+ @GuardedBy("lock")
+ @Nullable
+ private volatile OutputStream mTlsOutputStream;
+
+ /**
+ * The backend thread that handles responding to ADB packets.
+ */
+ @NonNull
+ private final Thread mConnectionThread;
+
+ /**
+ * Specifies whether a CNXN has been attempted.
+ */
+ private volatile boolean mConnectAttempted;
+
+ /**
+ * Whether the connection thread should give up if the first authentication attempt fails.
+ */
+ private volatile boolean mAbortOnUnauthorised;
+
+ /**
+ * Whether the first authentication attempt failed and {@link #mAbortOnUnauthorised} was {@code true}.
+ */
+ private volatile boolean mAuthorisationFailed;
+
+ /**
+ * Specifies whether a CNXN packet has been received from the peer.
+ */
+ private volatile boolean mConnectionEstablished;
+
+ /**
+ * Exceptions that occur in {@link #createConnectionThread()}.
+ */
+ @Nullable
+ private volatile Exception mConnectionException;
+
+ /**
+ * Specifies the maximum amount data that can be sent to the remote peer.
+ * This is only valid after connect() returns successfully.
+ */
+ private volatile int mMaxData;
+
+ private volatile int mProtocolVersion;
+
+ @NonNull
+ private final KeyPair mKeyPair;
+
+ @NonNull
+ private volatile String mDeviceName = "Unknown Device";
+
+ /**
+ * Specifies whether this connection has already sent a signed token.
+ */
+ private volatile boolean mSentSignature;
+
+ /**
+ * A hash map of our opened streams indexed by local ID.
+ */
+ @NonNull
+ private final ConcurrentHashMap mOpenedStreams;
+
+ private volatile boolean mIsTls = false;
+
+ @GuardedBy("lock")
+ @NonNull
+ private final Object mLock = new Object();
+
+ /**
+ * Creates a AdbConnection object associated with the socket and crypto object specified.
+ *
+ * @return A new AdbConnection object.
+ * @throws IOException If there is a socket error
+ */
+ @WorkerThread
+ @NonNull
+ public static AdbConnection create(@NonNull String host, int port, @NonNull PrivateKey privateKey,
+ @NonNull Certificate certificate)
+ throws IOException {
+ return create(host, port, privateKey, certificate, Build.VERSION_CODES.BASE);
+ }
+
+ /**
+ * Creates a AdbConnection object associated with the socket and crypto object specified.
+ *
+ * @return A new AdbConnection object.
+ * @throws IOException If there is a socket error
+ */
+ @WorkerThread
+ @NonNull
+ public static AdbConnection create(@NonNull String host, int port, @NonNull PrivateKey privateKey,
+ @NonNull Certificate certificate, int api)
+ throws IOException {
+ return create(host, port, new KeyPair(Objects.requireNonNull(privateKey), Objects.requireNonNull(certificate)),
+ api);
+ }
+
+ /**
+ * Creates a AdbConnection object associated with the socket and crypto object specified.
+ *
+ * @return A new AdbConnection object.
+ * @throws IOException If there is a socket error
+ */
+ @WorkerThread
+ @NonNull
+ static AdbConnection create(@NonNull String host, int port, @NonNull KeyPair keyPair, int api) throws IOException {
+ return new AdbConnection(host, port, keyPair, api);
+ }
+
+ /**
+ * Internal constructor to initialize some internal state
+ */
+ @WorkerThread
+ private AdbConnection(@NonNull String host, int port, @NonNull KeyPair keyPair, int api) throws IOException {
+ this.mHost = Objects.requireNonNull(host);
+ this.mPort = port;
+ this.mApi = api;
+ this.mProtocolVersion = AdbProtocol.getProtocolVersion(mApi);
+ this.mMaxData = AdbProtocol.getMaxData(api);
+ this.mKeyPair = Objects.requireNonNull(keyPair);
+ try {
+ this.mSocket = new Socket(host, port);
+ } catch (Throwable th) {
+ //noinspection UnnecessaryInitCause
+ throw (IOException) new IOException().initCause(th);
+ }
+ this.mPlainInputStream = mSocket.getInputStream();
+ this.mPlainOutputStream = mSocket.getOutputStream();
+
+ // Disable Nagle because we're sending tiny packets
+ mSocket.setTcpNoDelay(true);
+
+ this.mOpenedStreams = new ConcurrentHashMap<>();
+ this.mLastLocalId = 0;
+ this.mConnectionThread = createConnectionThread();
+ }
+
+ @GuardedBy("lock")
+ @NonNull
+ private InputStream getInputStream() {
+ return mIsTls ? Objects.requireNonNull(mTlsInputStream) : mPlainInputStream;
+ }
+
+ @GuardedBy("lock")
+ @NonNull
+ private OutputStream getOutputStream() {
+ return mIsTls ? Objects.requireNonNull(mTlsOutputStream) : mPlainOutputStream;
+ }
+
+ /**
+ * Creates a new connection thread.
+ *
+ * @return A new connection thread.
+ */
+ @NonNull
+ private Thread createConnectionThread() {
+ return new Thread(() -> {
+ loop:
+ while (!mConnectionThread.isInterrupted()) {
+ try {
+ // Read and parse a message off the socket's input stream
+ AdbProtocol.Message msg = AdbProtocol.Message.parse(getInputStream(), mProtocolVersion, mMaxData);
+
+ switch (msg.command) {
+ // Stream-oriented commands
+ case AdbProtocol.A_OKAY:
+ case AdbProtocol.A_WRTE:
+ case AdbProtocol.A_CLSE: {
+ // Ignore all packets when not connected
+ if (!mConnectionEstablished) {
+ continue;
+ }
+
+ // Get the stream object corresponding to the packet
+ AdbStream waitingStream = mOpenedStreams.get(msg.arg1);
+ if (waitingStream == null) {
+ continue;
+ }
+
+ synchronized (waitingStream) {
+ if (msg.command == AdbProtocol.A_OKAY) {
+ // We're ready for writes
+ waitingStream.updateRemoteId(msg.arg0);
+ waitingStream.readyForWrite();
+
+ // Notify an open/write
+ waitingStream.notify();
+ } else if (msg.command == AdbProtocol.A_WRTE) {
+ // Got some data from our partner
+ waitingStream.addPayload(msg.payload);
+
+ // Tell it we're ready for more
+ waitingStream.sendReady();
+ } else { // if (msg.command == AdbProtocol.A_CLSE) {
+ mOpenedStreams.remove(msg.arg1);
+ // Notify readers and writers
+ waitingStream.notifyClose(true);
+ }
+ }
+ break;
+ }
+ case AdbProtocol.A_STLS: {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ sendPacket(AdbProtocol.generateStls());
+
+ SSLContext sslContext = SslUtils.getSslContext(mKeyPair);
+ SSLSocket tlsSocket = (SSLSocket) sslContext.getSocketFactory()
+ .createSocket(mSocket, mHost, mPort, true);
+ tlsSocket.startHandshake();
+ Log.d(TAG, "Handshake succeeded.");
+
+ synchronized (AdbConnection.this) {
+ mTlsInputStream = tlsSocket.getInputStream();
+ mTlsOutputStream = tlsSocket.getOutputStream();
+ mIsTls = true;
+ }
+ }
+ break;
+ }
+ case AdbProtocol.A_AUTH: {
+ if (mIsTls) {
+ break;
+ }
+ if (msg.arg0 != AdbProtocol.ADB_AUTH_TOKEN) {
+ break;
+ }
+ byte[] packet;
+ // This is an authentication challenge
+ if (mSentSignature) {
+ if (mAbortOnUnauthorised) {
+ mAuthorisationFailed = true;
+ break loop;
+ }
+
+ // We've already tried our signature, so send our public key
+ packet = AdbProtocol.generateAuth(AdbProtocol.ADB_AUTH_RSAPUBLICKEY, AndroidPubkey
+ .encodeWithName((RSAPublicKey) mKeyPair.getPublicKey(), mDeviceName));
+ } else {
+ // Sign the token
+ packet = AdbProtocol.generateAuth(AdbProtocol.ADB_AUTH_SIGNATURE, AndroidPubkey
+ .adbAuthSign(mKeyPair.getPrivateKey(), msg.payload));
+ mSentSignature = true;
+ }
+
+ // Write the AUTH reply
+ sendPacket(packet);
+ break;
+ }
+ case AdbProtocol.A_CNXN: {
+ synchronized (AdbConnection.this) {
+ mProtocolVersion = msg.arg0;
+ mMaxData = msg.arg1;
+ mConnectionEstablished = true;
+ AdbConnection.this.notifyAll();
+ }
+ break;
+ }
+ case AdbProtocol.A_OPEN:
+ case AdbProtocol.A_SYNC:
+ default:
+ Log.e(TAG, String.format("Unrecognized command = 0x%x", msg.command));
+ // Unrecognized packet, just drop it
+ break;
+ }
+ } catch (Exception e) {
+ mConnectionException = e;
+ e.printStackTrace();
+ // The cleanup is taken care of by a combination of this thread and close()
+ break;
+ }
+ }
+
+ // This thread takes care of cleaning up pending streams
+ synchronized (AdbConnection.this) {
+ cleanupStreams();
+ AdbConnection.this.notifyAll();
+ mConnectionEstablished = false;
+ mConnectAttempted = false;
+ }
+ });
+ }
+
+ /**
+ * Set a name for the device. Default is “Unknown Device”.
+ *
+ * @param deviceName Name of the device, could be the app label, hostname or user@hostname.
+ */
+ public void setDeviceName(@NonNull String deviceName) {
+ this.mDeviceName = Objects.requireNonNull(deviceName);
+ }
+
+ /**
+ * Get the version of the ADB protocol supported by the ADB daemon. The result may depend on the API version
+ * specified and whether the connection has been established. In API 29 (Android 9) or later, the daemon returns
+ * {@link AdbProtocol#A_VERSION_SKIP_CHECKSUM} regardless of the protocol used to create the connection. So, if
+ * {@link #mApi} is set to API 28 or earlier but the OS version is Android 9 or later, before establishing the
+ * connection, it returns {@link AdbProtocol#A_VERSION_MIN}, and after establishing the connection, it returns
+ * {@link AdbProtocol#A_VERSION_SKIP_CHECKSUM}. In other cases, it always returns {@link AdbProtocol#A_VERSION_MIN}.
+ *
+ * @see #isConnectionEstablished()
+ */
+ public int getProtocolVersion() {
+ return mProtocolVersion;
+ }
+
+ /**
+ * Get the max data size supported by the ADB daemon. A connection have to be attempted before calling this method
+ * and shall be blocked if the connection is in progress.
+ *
+ * @return The maximum data size indicated in the CONNECT packet.
+ * @throws InterruptedException If a connection cannot be waited on.
+ * @throws IOException if the connection fails.
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ public int getMaxData() throws InterruptedException, IOException, AdbPairingRequiredException {
+ if (!mConnectAttempted) {
+ throw new IllegalStateException("connect() must be called first");
+ }
+
+ waitForConnection(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+
+ return mMaxData;
+ }
+
+ /**
+ * Whether a connection has been established. A connection has been established if a CONNECT request has been
+ * received from the ADB daemon.
+ */
+ public boolean isConnectionEstablished() {
+ return mConnectionEstablished;
+ }
+
+ /**
+ * Whether the underlying socket is connected to an ADB daemon and is not in a closed state.
+ */
+ public boolean isConnected() {
+ return !mSocket.isClosed() && mSocket.isConnected();
+ }
+
+ /**
+ * Same as {@link #connect(long, TimeUnit, boolean)} without throwing anything if the first authentication attempt
+ * fails.
+ *
+ * @return {@code true} if the connection was established, or {@code false} if the connection timed out
+ * @throws IOException If the socket fails while connecting
+ * @throws InterruptedException If timeout has reached
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ public boolean connect() throws IOException, InterruptedException, AdbPairingRequiredException {
+ return connect(Long.MAX_VALUE, TimeUnit.MILLISECONDS, false);
+ }
+
+ /**
+ * Connects to the remote device. This routine will block until the connection completes or the timeout elapses.
+ *
+ * @param timeout the time to wait for the lock
+ * @param unit the time unit of the timeout argument
+ * @param throwOnUnauthorised Whether to throw an {@link AdbAuthenticationFailedException}
+ * if the peer rejects out first authentication attempt
+ * @return {@code true} if the connection was established, or {@code false} if the connection timed out
+ * @throws IOException If the socket fails while connecting
+ * @throws InterruptedException If timeout has reached
+ * @throws AdbAuthenticationFailedException If {@code throwOnUnauthorised} is {@code true} and the peer rejects the
+ * first authentication attempt, which indicates that the peer has not
+ * saved the public key from a previous connection
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ public boolean connect(long timeout, @NonNull TimeUnit unit, boolean throwOnUnauthorised)
+ throws IOException, InterruptedException, AdbAuthenticationFailedException, AdbPairingRequiredException {
+ if (mConnectionEstablished) {
+ throw new IllegalStateException("Already connected");
+ }
+
+ // Send CONNECT
+ sendPacket(AdbProtocol.generateConnect(mApi));
+
+ // Start the connection thread to respond to the peer
+ mConnectAttempted = true;
+ mAbortOnUnauthorised = throwOnUnauthorised;
+ mAuthorisationFailed = false;
+ mConnectionThread.start();
+
+ return waitForConnection(timeout, Objects.requireNonNull(unit));
+ }
+
+ /**
+ * Opens an {@link AdbStream} object corresponding to the specified destination.
+ * This routine will block until the connection completes.
+ *
+ * @param service The service to open. One of the services under {@link LocalServices.Services}.
+ * @param args Additional arguments supported by the service (see the corresponding constant to learn more).
+ * @return AdbStream object corresponding to the specified destination
+ * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8
+ * @throws IOException If the stream fails while sending the packet
+ * @throws InterruptedException If we are unable to wait for the connection to finish
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ @NonNull
+ public AdbStream open(@LocalServices.Services int service, @NonNull String... args)
+ throws IOException, InterruptedException, AdbPairingRequiredException {
+ if (service < LocalServices.SERVICE_FIRST || service > LocalServices.SERVICE_LAST) {
+ throw new IllegalArgumentException("Invalid service: " + service);
+ }
+ return open(LocalServices.getDestination(service, args));
+ }
+
+ /**
+ * Opens an AdbStream object corresponding to the specified destination.
+ * This routine will block until the connection completes.
+ *
+ * @param destination The destination to open on the target
+ * @return AdbStream object corresponding to the specified destination
+ * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8
+ * @throws IOException If the stream fails while sending the packet
+ * @throws InterruptedException If we are unable to wait for the connection to finish
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ @NonNull
+ public AdbStream open(@NonNull String destination)
+ throws IOException, InterruptedException, AdbPairingRequiredException {
+ int localId = ++mLastLocalId;
+
+ if (!mConnectAttempted) {
+ throw new IllegalStateException("connect() must be called first");
+ }
+
+ waitForConnection(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+
+ // Add this stream to this list of half-open streams
+ AdbStream stream = new AdbStream(this, localId);
+ mOpenedStreams.put(localId, stream);
+
+ // Send OPEN
+ sendPacket(AdbProtocol.generateOpen(localId, Objects.requireNonNull(destination)));
+
+ // Wait for the connection thread to receive the OKAY
+ synchronized (stream) {
+ stream.wait();
+ }
+
+ // Check if the OPEN request was rejected
+ if (stream.isClosed()) {
+ mOpenedStreams.remove(localId);
+ throw new ConnectException("Stream open actively rejected by remote peer.");
+ }
+
+ return stream;
+ }
+
+ private boolean waitForConnection(long timeout, @NonNull TimeUnit unit)
+ throws InterruptedException, IOException, AdbPairingRequiredException {
+ synchronized (this) {
+ // Block if a connection is pending, but not yet complete
+ long timeoutEndMillis = System.currentTimeMillis() + Objects.requireNonNull(unit).toMillis(timeout);
+ while (!mConnectionEstablished && mConnectAttempted && timeoutEndMillis - System.currentTimeMillis() > 0) {
+ wait(timeoutEndMillis - System.currentTimeMillis());
+ }
+
+ if (!mConnectionEstablished) {
+ if (mConnectAttempted) {
+ return false;
+ } else if (mAuthorisationFailed) {
+ // The peer may not have saved the public key in the past connections, or they've been removed.
+ throw new AdbAuthenticationFailedException();
+ } else {
+ Exception connectionException = mConnectionException;
+ if (connectionException != null) {
+ if (connectionException instanceof javax.net.ssl.SSLProtocolException) {
+ String message = connectionException.getMessage();
+ if (message != null && message.contains("protocol error")) {
+ throw (AdbPairingRequiredException) (new AdbPairingRequiredException("ADB pairing is required.").initCause(connectionException));
+ }
+ }
+ }
+ throw new IOException("Connection failed");
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * This function terminates all I/O on streams associated with this ADB connection
+ */
+ private void cleanupStreams() {
+ // Close all streams on this connection
+ for (AdbStream s : mOpenedStreams.values()) {
+ try {
+ s.close();
+ } catch (IOException ignored) {
+ }
+ }
+ mOpenedStreams.clear();
+ }
+
+ /**
+ * This routine closes the Adb connection and underlying socket
+ *
+ * @throws IOException if the socket fails to close
+ */
+ @Override
+ public void close() throws IOException {
+ // Closing the socket will kick the connection thread
+ mSocket.close();
+
+ // Wait for the connection thread to die
+ mConnectionThread.interrupt();
+ try {
+ mConnectionThread.join();
+ } catch (InterruptedException ignored) {
+ }
+
+ // Destroy keypair
+ try {
+ mKeyPair.destroy();
+ } catch (DestroyFailedException ignore) {
+ }
+ }
+
+ void sendPacket(byte[] packet) throws IOException {
+ synchronized (mLock) {
+ OutputStream os = getOutputStream();
+ os.write(packet);
+ os.flush();
+ }
+ }
+
+ void flushPacket() throws IOException {
+ synchronized (mLock) {
+ getOutputStream().flush();
+ }
+ }
+
+ public static class Builder {
+ private String mHost = "127.0.0.1";
+ private int mPort = 5555;
+ private int mApi = Build.VERSION_CODES.BASE;
+ private PrivateKey mPrivateKey;
+ private Certificate mCertificate;
+ private KeyPair mKeyPair;
+ private String mDeviceName;
+
+ public Builder() {
+ }
+
+ public Builder(String host, int port) {
+ mHost = host;
+ mPort = port;
+ }
+
+ /**
+ * Set host address. Default is 127.0.0.1
+ */
+ public Builder setHost(String host) {
+ this.mHost = host;
+ return this;
+ }
+
+ /**
+ * Set port number. Default is 5555.
+ */
+ public Builder setPort(int port) {
+ this.mPort = port;
+ return this;
+ }
+
+ /**
+ * Set a name for the device. Default is “Unknown Device”.
+ *
+ * @param deviceName Name of the device, could be the app label, hostname or user@hostname.
+ */
+ public Builder setDeviceName(String deviceName) {
+ this.mDeviceName = deviceName;
+ return this;
+ }
+
+ /**
+ * Set Android API (i.e. SDK) version for this connection. If the ADB daemon and the client are located in the
+ * same device, the value should be {@link Build.VERSION#SDK_INT} in order to improve performance as well as
+ * security.
+ *
+ * @param api The API version, default is {@link Build.VERSION_CODES#BASE}.
+ */
+ public Builder setApi(int api) {
+ this.mApi = api;
+ return this;
+ }
+
+ /**
+ * Set generated/stored private key.
+ */
+ public Builder setPrivateKey(PrivateKey privateKey) {
+ this.mPrivateKey = privateKey;
+ return this;
+ }
+
+ /**
+ * Set public key wrapped around a certificate
+ */
+ public Builder setCertificate(Certificate certificate) {
+ this.mCertificate = certificate;
+ return this;
+ }
+
+ Builder setKeyPair(KeyPair keyPair) {
+ this.mKeyPair = keyPair;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link AdbConnection} associated with the socket and crypto object specified.
+ *
+ * @throws IOException If there was an error while establishing a socket connection
+ */
+ public AdbConnection build() throws IOException {
+ if (mKeyPair == null) {
+ if (mPrivateKey == null || mCertificate == null) {
+ throw new UnsupportedOperationException("Private key and certificate must be set.");
+ }
+ mKeyPair = new KeyPair(mPrivateKey, mCertificate);
+ }
+ AdbConnection adbConnection = create(mHost, mPort, mKeyPair, mApi);
+ if (mDeviceName != null) {
+ adbConnection.setDeviceName(mDeviceName);
+ }
+ return adbConnection;
+ }
+
+ /**
+ * Same as {@link #connect(long, TimeUnit, boolean)} without throwing anything if the first authentication
+ * attempt fails.
+ *
+ * @return The underlying {@link AdbConnection}
+ * @throws IOException If the socket fails while connecting
+ * @throws InterruptedException If timeout has reached
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ public AdbConnection connect() throws IOException, InterruptedException, AdbPairingRequiredException {
+ AdbConnection adbConnection = build();
+ if (adbConnection.connect()) {
+ throw new IOException("Unable to establish a new connection.");
+ }
+ return adbConnection;
+ }
+
+ /**
+ * Connects to the remote device. This routine will block until the connection completes or the timeout elapses.
+ *
+ * @param timeout the time to wait for the lock
+ * @param unit the time unit of the timeout argument
+ * @param throwOnUnauthorised Whether to throw an {@link AdbAuthenticationFailedException}
+ * if the peer rejects out first authentication attempt
+ * @return {@code true} if the connection was established, or {@code false} if the connection timed out
+ * @throws IOException If the socket fails while connecting
+ * @throws InterruptedException If timeout has reached
+ * @throws AdbAuthenticationFailedException If {@code throwOnUnauthorised} is {@code true} and the peer rejects
+ * the first authentication attempt, which indicates that the peer has
+ * not saved the public key from a previous connection
+ * @throws AdbPairingRequiredException If ADB lacks pairing
+ */
+ public AdbConnection connect(long timeout, @NonNull TimeUnit unit, boolean throwOnUnauthorised)
+ throws IOException, InterruptedException, AdbPairingRequiredException {
+ AdbConnection adbConnection = build();
+ if (adbConnection.connect(timeout, unit, throwOnUnauthorised)) {
+ throw new IOException("Unable to establish a new connection.");
+ }
+ return adbConnection;
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnectionManager.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnectionManager.java
new file mode 100644
index 0000000..4b5fa20
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnectionManager.java
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import android.content.Context;
+import android.os.Build;
+import android.sun.misc.BASE64Encoder;
+import android.sun.security.provider.X509Factory;
+import android.sun.security.x509.AlgorithmId;
+import android.sun.security.x509.CertificateAlgorithmId;
+import android.sun.security.x509.CertificateExtensions;
+import android.sun.security.x509.CertificateIssuerName;
+import android.sun.security.x509.CertificateSerialNumber;
+import android.sun.security.x509.CertificateSubjectName;
+import android.sun.security.x509.CertificateValidity;
+import android.sun.security.x509.CertificateVersion;
+import android.sun.security.x509.CertificateX509Key;
+import android.sun.security.x509.KeyIdentifier;
+import android.sun.security.x509.PrivateKeyUsageExtension;
+import android.sun.security.x509.SubjectKeyIdentifierExtension;
+import android.sun.security.x509.X500Name;
+import android.sun.security.x509.X509CertImpl;
+import android.sun.security.x509.X509CertInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.spec.EncodedKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Date;
+import java.util.Random;
+
+
+public class AdbConnectionManager extends AbsAdbConnectionManager {
+ private static AbsAdbConnectionManager INSTANCE;
+
+ public static AbsAdbConnectionManager getInstance(@NonNull Context context) throws Exception {
+ if (INSTANCE == null) {
+ INSTANCE = new AdbConnectionManager(context);
+ }
+ return INSTANCE;
+ }
+
+ private PrivateKey mPrivateKey;
+ private Certificate mCertificate;
+
+ private AdbConnectionManager(@NonNull Context context) throws Exception {
+ setApi(Build.VERSION.SDK_INT);
+ mPrivateKey = readPrivateKeyFromFile(context);
+ mCertificate = readCertificateFromFile(context);
+ if (mPrivateKey == null) {
+ // Generate a new key pair
+ int keySize = 2048;
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(keySize, SecureRandom.getInstance("SHA1PRNG"));
+ KeyPair generateKeyPair = keyPairGenerator.generateKeyPair();
+ PublicKey publicKey = generateKeyPair.getPublic();
+ mPrivateKey = generateKeyPair.getPrivate();
+ // Generate a new certificate
+ String subject = "CN=My Awesome App";
+ String algorithmName = "SHA512withRSA";
+ long expiryDate = System.currentTimeMillis() + 86400000;
+ CertificateExtensions certificateExtensions = new CertificateExtensions();
+ certificateExtensions.set("SubjectKeyIdentifier", new SubjectKeyIdentifierExtension(
+ new KeyIdentifier(publicKey).getIdentifier()));
+ X500Name x500Name = new X500Name(subject);
+ Date notBefore = new Date();
+ Date notAfter = new Date(expiryDate);
+ certificateExtensions.set("PrivateKeyUsage", new PrivateKeyUsageExtension(notBefore, notAfter));
+ CertificateValidity certificateValidity = new CertificateValidity(notBefore, notAfter);
+ X509CertInfo x509CertInfo = new X509CertInfo();
+ x509CertInfo.set("version", new CertificateVersion(2));
+ x509CertInfo.set("serialNumber", new CertificateSerialNumber(new Random().nextInt() & Integer.MAX_VALUE));
+ x509CertInfo.set("algorithmID", new CertificateAlgorithmId(AlgorithmId.get(algorithmName)));
+ x509CertInfo.set("subject", new CertificateSubjectName(x500Name));
+ x509CertInfo.set("key", new CertificateX509Key(publicKey));
+ x509CertInfo.set("validity", certificateValidity);
+ x509CertInfo.set("issuer", new CertificateIssuerName(x500Name));
+ x509CertInfo.set("extensions", certificateExtensions);
+ X509CertImpl x509CertImpl = new X509CertImpl(x509CertInfo);
+ x509CertImpl.sign(mPrivateKey, algorithmName);
+ mCertificate = x509CertImpl;
+ // Write files
+ writePrivateKeyToFile(context, mPrivateKey);
+ writeCertificateToFile(context, mCertificate);
+ }
+ }
+
+ @NonNull
+ @Override
+ protected PrivateKey getPrivateKey() {
+ return mPrivateKey;
+ }
+
+ @NonNull
+ @Override
+ protected Certificate getCertificate() {
+ return mCertificate;
+ }
+
+ @NonNull
+ @Override
+ protected String getDeviceName() {
+ return "MyAwesomeApp";
+ }
+
+ @Nullable
+ private static Certificate readCertificateFromFile(@NonNull Context context)
+ throws IOException, CertificateException {
+ File certFile = new File(context.getFilesDir(), "cert.pem");
+ if (!certFile.exists()) return null;
+ try (InputStream cert = new FileInputStream(certFile)) {
+ return CertificateFactory.getInstance("X.509").generateCertificate(cert);
+ }
+ }
+
+ private static void writeCertificateToFile(@NonNull Context context, @NonNull Certificate certificate)
+ throws CertificateEncodingException, IOException {
+ File certFile = new File(context.getFilesDir(), "cert.pem");
+ BASE64Encoder encoder = new BASE64Encoder();
+ try (OutputStream os = new FileOutputStream(certFile)) {
+ os.write(X509Factory.BEGIN_CERT.getBytes(StandardCharsets.UTF_8));
+ os.write('\n');
+ encoder.encode(certificate.getEncoded(), os);
+ os.write('\n');
+ os.write(X509Factory.END_CERT.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ @Nullable
+ private static PrivateKey readPrivateKeyFromFile(@NonNull Context context)
+ throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
+ File privateKeyFile = new File(context.getFilesDir(), "private.key");
+ if (!privateKeyFile.exists()) return null;
+ byte[] privKeyBytes = new byte[(int) privateKeyFile.length()];
+ try (InputStream is = new FileInputStream(privateKeyFile)) {
+ is.read(privKeyBytes);
+ }
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);
+ return keyFactory.generatePrivate(privateKeySpec);
+ }
+
+ private static void writePrivateKeyToFile(@NonNull Context context, @NonNull PrivateKey privateKey)
+ throws IOException {
+ File privateKeyFile = new File(context.getFilesDir(), "private.key");
+ try (OutputStream os = new FileOutputStream(privateKeyFile)) {
+ os.write(privateKey.getEncoded());
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbInputStream.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbInputStream.java
new file mode 100644
index 0000000..eabcc36
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbInputStream.java
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AdbInputStream extends InputStream {
+ public AdbStream mAdbStream;
+
+ public AdbInputStream(AdbStream adbStream) {
+ this.mAdbStream = adbStream;
+ }
+
+ @Override
+ public int read() throws IOException {
+ byte[] bytes = new byte[1];
+ if (read(bytes) == -1) {
+ return -1;
+ }
+ return bytes[0];
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (mAdbStream.isClosed()) return -1;
+ return mAdbStream.read(b, off, len);
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public int available() throws IOException {
+ return mAdbStream.available();
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbOutputStream.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbOutputStream.java
new file mode 100644
index 0000000..6eea18e
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbOutputStream.java
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class AdbOutputStream extends OutputStream {
+ private final AdbStream mAdbStream;
+
+ public AdbOutputStream(AdbStream adbStream) {
+ this.mAdbStream = adbStream;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ write(new byte[]{(byte) (b & 0xFF)});
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ mAdbStream.write(b, off, len);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ mAdbStream.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ flush();
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbPairingRequiredException.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbPairingRequiredException.java
new file mode 100644
index 0000000..59e0e57
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbPairingRequiredException.java
@@ -0,0 +1,7 @@
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+public class AdbPairingRequiredException extends Exception {
+ public AdbPairingRequiredException(String message) {
+ super(message);
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbProtocol.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbProtocol.java
new file mode 100644
index 0000000..fe56823
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbProtocol.java
@@ -0,0 +1,497 @@
+// SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0)
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import android.os.Build;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StreamCorruptedException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/**
+ * This class provides useful functions and fields for ADB protocol details.
+ */
+// Copyright 2013 Cameron Gutman
+final class AdbProtocol {
+ /**
+ * The length of the ADB message header
+ */
+ public static final int ADB_HEADER_LENGTH = 24;
+
+ /**
+ * SYNC(online, sequence, "")
+ *
+ * @deprecated Obsolete, no longer used. Never used on the client side.
+ */
+ public static final int A_SYNC = 0x434e5953;
+
+ /**
+ * CNXN is the connect message. No messages (except AUTH) are valid before this message is received.
+ */
+ public static final int A_CNXN = 0x4e584e43;
+
+ /**
+ * The payload sent with the CONNECT message.
+ */
+ public static final byte[] SYSTEM_IDENTITY_STRING_HOST = StringCompat.getBytes("host::\0", "UTF-8");
+
+ /**
+ * AUTH is the authentication message. It is part of the RSA public key authentication added in Android 4.2.2
+ * ({@link Build.VERSION_CODES#JELLY_BEAN_MR1}).
+ */
+ public static final int A_AUTH = 0x48545541;
+
+ /**
+ * OPEN is the open stream message. It is sent to open a new stream on the target device.
+ */
+ public static final int A_OPEN = 0x4e45504f;
+
+ /**
+ * OKAY is a success message. It is sent when a write is processed successfully.
+ */
+ public static final int A_OKAY = 0x59414b4f;
+
+ /**
+ * CLSE is the close stream message. It is sent to close an existing stream on the target device.
+ */
+ public static final int A_CLSE = 0x45534c43;
+
+ /**
+ * WRTE is the write stream message. It is sent with a payload that is the data to write to the stream.
+ */
+ public static final int A_WRTE = 0x45545257;
+
+ /**
+ * STLS is the Stream-based TLS1.3 authentication method, added in Android 9 ({@link Build.VERSION_CODES#P}).
+ */
+ public static final int A_STLS = 0x534c5453;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({A_SYNC, A_CNXN, A_OPEN, A_OKAY, A_CLSE, A_WRTE, A_AUTH, A_STLS})
+ private @interface Command {
+ }
+
+ /**
+ * Original payload size
+ */
+ public static final int MAX_PAYLOAD_V1 = 4 * 1024;
+ /**
+ * Supported payload size since Android 7 (N)
+ */
+ public static final int MAX_PAYLOAD_V2 = 256 * 1024;
+ /**
+ * Supported payload size since Android 9 (P)
+ */
+ public static final int MAX_PAYLOAD_V3 = 1024 * 1024;
+ /**
+ * Maximum supported payload size is set to the original to support all APIs
+ */
+ public static final int MAX_PAYLOAD = MAX_PAYLOAD_V1;
+
+ /**
+ * The original version of the ADB protocol
+ */
+ public static final int A_VERSION_MIN = 0x01000000;
+ /**
+ * The new version of the ADB protocol introduced in Android 9 (P) with the introduction of TLS
+ */
+ public static final int A_VERSION_SKIP_CHECKSUM = 0x01000001;
+ public static final int A_VERSION = A_VERSION_MIN;
+
+ /**
+ * The current version of the Stream-based TLS
+ */
+ public static final int A_STLS_VERSION_MIN = 0x01000000;
+ public static final int A_STLS_VERSION = A_STLS_VERSION_MIN;
+
+ /**
+ * This authentication type represents a SHA1 hash to sign.
+ */
+ public static final int ADB_AUTH_TOKEN = 1;
+
+ /**
+ * This authentication type represents the signed SHA1 hash.
+ */
+ public static final int ADB_AUTH_SIGNATURE = 2;
+
+ /**
+ * This authentication type represents an RSA public key.
+ */
+ public static final int ADB_AUTH_RSAPUBLICKEY = 3;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ADB_AUTH_TOKEN, ADB_AUTH_SIGNATURE, ADB_AUTH_RSAPUBLICKEY})
+ private @interface AuthType {
+ }
+
+ public static int getMaxData(int api) {
+ if (api >= Build.VERSION_CODES.P) {
+ return MAX_PAYLOAD_V3;
+ }
+ if (api >= Build.VERSION_CODES.N) {
+ return MAX_PAYLOAD_V2;
+ }
+ return MAX_PAYLOAD_V1;
+ }
+
+ public static int getProtocolVersion(int api) {
+ if (api >= Build.VERSION_CODES.P) {
+ return A_VERSION_SKIP_CHECKSUM;
+ }
+ return A_VERSION_MIN;
+ }
+
+ /**
+ * This function performs a checksum on the ADB payload data.
+ *
+ * @param data The data
+ * @return The checksum of the data
+ */
+ private static int getPayloadChecksum(@NonNull byte[] data) {
+ return getPayloadChecksum(data, 0, data.length);
+ }
+
+ /**
+ * This function performs a checksum on the ADB payload data.
+ *
+ * @param data The data
+ * @param offset The start offset in the data
+ * @param length The number of bytes to take from the data
+ * @return The checksum of the data
+ */
+ private static int getPayloadChecksum(@NonNull byte[] data, int offset, int length) {
+ int checksum = 0;
+ for (int i = offset; i < offset + length; ++i) {
+ checksum += data[i] & 0xFF;
+ }
+ return checksum;
+ }
+
+ /**
+ * This function generates an ADB message given the fields.
+ *
+ * @param command Command identifier constant
+ * @param arg0 First argument
+ * @param arg1 Second argument
+ * @param data The data
+ * @return Byte array containing the message
+ */
+ @NonNull
+ public static byte[] generateMessage(@Command int command, int arg0, int arg1, @Nullable byte[] data) {
+ return generateMessage(command, arg0, arg1, data, 0, data == null ? 0 : data.length);
+ }
+
+ /**
+ * This function generates an ADB message given the fields.
+ *
+ * @param command Command identifier constant
+ * @param arg0 First argument
+ * @param arg1 Second argument
+ * @param data The data
+ * @param offset The start offset in the data
+ * @param length The number of bytes to take from the data
+ * @return Byte array containing the message
+ */
+ @NonNull
+ public static byte[] generateMessage(@Command int command, int arg0, int arg1, @Nullable byte[] data, int offset, int length) {
+ // Protocol as defined at https://github.com/aosp-mirror/platform_system_core/blob/6072de17cd812daf238092695f26a552d3122f8c/adb/protocol.txt
+ // struct message {
+ // unsigned command; // command identifier constant
+ // unsigned arg0; // first argument
+ // unsigned arg1; // second argument
+ // unsigned data_length; // length of payload (0 is allowed)
+ // unsigned data_check; // checksum of data payload
+ // unsigned magic; // command ^ 0xffffffff
+ // };
+
+ ByteBuffer message;
+
+ if (data != null) {
+ message = ByteBuffer.allocate(ADB_HEADER_LENGTH + length).order(ByteOrder.LITTLE_ENDIAN);
+ } else {
+ message = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ message.putInt(command);
+ message.putInt(arg0);
+ message.putInt(arg1);
+
+ if (data != null) {
+ message.putInt(length);
+ message.putInt(getPayloadChecksum(data, offset, length));
+ } else {
+ message.putInt(0);
+ message.putInt(0);
+ }
+
+ message.putInt(~command);
+
+ if (data != null) {
+ message.put(data, offset, length);
+ }
+
+ return message.array();
+ }
+
+ /**
+ * Generates a CONNECT message for a given API.
+ *
+ * CONNECT(version, maxdata, "system-identity-string")
+ *
+ * @param api API version
+ * @return Byte array containing the message
+ */
+ @NonNull
+ public static byte[] generateConnect(int api) {
+ return generateMessage(A_CNXN, getProtocolVersion(api), getMaxData(api), SYSTEM_IDENTITY_STRING_HOST);
+ }
+
+ /**
+ * Generates an AUTH message with the specified type and payload.
+ *
+ * AUTH(type, 0, "data")
+ *
+ * @param type Authentication type (see ADB_AUTH_* constants)
+ * @param data The data
+ * @return Byte array containing the message
+ */
+ @NonNull
+ public static byte[] generateAuth(@AuthType int type, byte[] data) {
+ return generateMessage(A_AUTH, type, 0, data);
+ }
+
+ /**
+ * Generates an STLS message with default parameters.
+ *
+ * STLS(version, 0, "")
+ *
+ * @return Byte array containing the message
+ */
+ @NonNull
+ public static byte[] generateStls() {
+ return generateMessage(A_STLS, A_STLS_VERSION, 0, null);
+ }
+
+ /**
+ * Generates an OPEN stream message with the specified local ID and destination.
+ *
+ * OPEN(local-id, 0, "destination")
+ *
+ * @param localId A unique local ID identifying the stream
+ * @param destination The destination of the stream on the target
+ * @return Byte array containing the message
+ */
+ @NonNull
+ public static byte[] generateOpen(int localId, @NonNull String destination) {
+ ByteBuffer bbuf = ByteBuffer.allocate(destination.length() + 100);
+ bbuf.put(StringCompat.getBytes(destination, "UTF-8"));
+ bbuf.put((byte) 0);
+ return generateMessage(A_OPEN, localId, 0, bbuf.array());
+ }
+
+ /**
+ * Generates a WRITE stream message with the specified IDs and payload.
+ *
+ * WRITE(local-id, remote-id, "data")
+ *
+ * @param localId The unique local ID of the stream
+ * @param remoteId The unique remote ID of the stream
+ * @param data The data
+ * @param offset The start offset in the data
+ * @param length The number of bytes to take from the data
+ * @return Byte array containing the message
+ */
+ @NonNull
+ public static byte[] generateWrite(int localId, int remoteId, byte[] data, int offset, int length) {
+ return generateMessage(A_WRTE, localId, remoteId, data, offset, length);
+ }
+
+ /**
+ * Generates a CLOSE stream message with the specified IDs.
+ *
+ * CLOSE(local-id, remote-id, "")
+ *
+ * @param localId The unique local ID of the stream
+ * @param remoteId The unique remote ID of the stream
+ * @return Byte array containing the message
+ */
+ @NonNull
+ public static byte[] generateClose(int localId, int remoteId) {
+ return generateMessage(A_CLSE, localId, remoteId, null);
+ }
+
+ /**
+ * Generates an OKAY/READY message with the specified IDs.
+ *
+ * READY(local-id, remote-id, "")
+ *
+ * @param localId The unique local ID of the stream
+ * @param remoteId The unique remote ID of the stream
+ * @return Byte array containing the message
+ */
+ @NonNull
+ public static byte[] generateReady(int localId, int remoteId) {
+ return generateMessage(A_OKAY, localId, remoteId, null);
+ }
+
+ /**
+ * This class provides an abstraction for the ADB message format.
+ */
+ static final class Message {
+ /**
+ * The command field of the message
+ */
+ @Command
+ public final int command;
+ /**
+ * The arg0 field of the message
+ */
+ public final int arg0;
+ /**
+ * The arg1 field of the message
+ */
+ public final int arg1;
+ /**
+ * The payload length field of the message
+ */
+ public final int dataLength;
+ /**
+ * The checksum field of the message
+ */
+ public final int dataCheck;
+ /**
+ * The magic field of the message
+ */
+ public final int magic;
+ /**
+ * The payload of the message
+ */
+ public byte[] payload;
+
+ /**
+ * Read and parse an ADB message from the supplied input stream.
+ *
+ * Note: If data is corrupted, the connection has to be closed immediately to avoid inconsistencies.
+ *
+ * @param in InputStream object to read data from
+ * @return An AdbMessage object represented the message read
+ * @throws IOException If the stream fails while reading.
+ * @throws StreamCorruptedException If data is corrupted.
+ */
+ @NonNull
+ public static Message parse(@NonNull InputStream in, int protocolVersion, int maxData) throws IOException {
+ ByteBuffer header = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
+
+ // Read header
+ int dataRead = 0;
+ do {
+ int bytesRead = in.read(header.array(), dataRead, ADB_HEADER_LENGTH - dataRead);
+ if (bytesRead < 0) {
+ throw new IOException("Stream closed");
+ } else dataRead += bytesRead;
+ } while (dataRead < ADB_HEADER_LENGTH);
+
+ Message msg = new Message(header);
+
+ // Validate header
+ if (msg.command != (~msg.magic)) { // magic = cmd ^ 0xFFFFFFFF
+ throw new StreamCorruptedException(String.format("Invalid header: Invalid magic 0x%x.", msg.magic));
+ }
+ if (msg.command != A_SYNC && msg.command != A_CNXN && msg.command != A_OPEN && msg.command != A_OKAY
+ && msg.command != A_CLSE && msg.command != A_WRTE && msg.command != A_AUTH
+ && msg.command != A_STLS) {
+ throw new StreamCorruptedException(String.format("Invalid header: Invalid command 0x%x.", msg.command));
+ }
+ if (msg.dataLength < 0 || msg.dataLength > maxData) {
+ throw new StreamCorruptedException(String.format("Invalid header: Invalid data length %d", msg.dataLength));
+ }
+
+ if (msg.dataLength == 0) {
+ // No payload supplied, return immediately
+ return msg;
+ }
+
+ // Read payload
+ msg.payload = new byte[msg.dataLength];
+ dataRead = 0;
+ do {
+ int bytesRead = in.read(msg.payload, dataRead, msg.dataLength - dataRead);
+ if (bytesRead < 0) {
+ throw new IOException("Stream closed");
+ } else dataRead += bytesRead;
+ } while (dataRead < msg.dataLength);
+
+ // Verify payload
+ if ((protocolVersion <= A_VERSION_MIN || (msg.command == A_CNXN && msg.arg0 <= A_VERSION_MIN))
+ && getPayloadChecksum(msg.payload) != msg.dataCheck) {
+ // Checksum verification failed
+ throw new StreamCorruptedException("Invalid header: Checksum mismatched.");
+ }
+
+ return msg;
+ }
+
+ private Message(@NonNull ByteBuffer header) {
+ command = header.getInt();
+ arg0 = header.getInt();
+ arg1 = header.getInt();
+ dataLength = header.getInt();
+ dataCheck = header.getInt();
+ magic = header.getInt();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ String tag;
+ switch (command) {
+ case A_SYNC:
+ tag = "SYNC";
+ break;
+ case A_CNXN:
+ tag = "CNXN";
+ break;
+ case A_OPEN:
+ tag = "OPEN";
+ break;
+ case A_OKAY:
+ tag = "OKAY";
+ break;
+ case A_CLSE:
+ tag = "CLSE";
+ break;
+ case A_WRTE:
+ tag = "WRTE";
+ break;
+ case A_AUTH:
+ tag = "AUTH";
+ break;
+ case A_STLS:
+ tag = "STLS";
+ break;
+ default:
+ tag = "????";
+ break;
+ }
+ return "Message{" +
+ "command=" + tag +
+ ", arg0=0x" + Integer.toHexString(arg0) +
+ ", arg1=0x" + Integer.toHexString(arg1) +
+ ", payloadLength=" + dataLength +
+ ", checksum=" + dataCheck +
+ ", magic=0x" + Integer.toHexString(magic) +
+ ", payload=" + Arrays.toString(payload) +
+ '}';
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java
new file mode 100644
index 0000000..02a019a
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java
@@ -0,0 +1,300 @@
+// SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0)
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * This class abstracts the underlying ADB streams
+ */
+// Copyright 2013 Cameron Gutman
+public class AdbStream implements Closeable {
+
+ /**
+ * The AdbConnection object that the stream communicates over
+ */
+ private final AdbConnection mAdbConnection;
+
+ /**
+ * The local ID of the stream
+ */
+ private final int mLocalId;
+
+ /**
+ * The remote ID of the stream
+ */
+ private volatile int mRemoteId;
+
+ /**
+ * Indicates whether WRTE is currently allowed
+ */
+ private final AtomicBoolean mWriteReady;
+
+ /**
+ * A queue of data from the target's WRTE packets
+ */
+ private final Queue mReadQueue;
+
+ /**
+ * Store data received from the first WRTE packet in order to support buffering.
+ */
+ private final ByteBuffer mReadBuffer;
+
+ /**
+ * Indicates whether the connection is closed already
+ */
+ private volatile boolean mIsClosed;
+
+ /**
+ * Whether the remote peer has closed but we still have unread data in the queue
+ */
+ private volatile boolean mPendingClose;
+
+ /**
+ * Creates a new AdbStream object on the specified AdbConnection
+ * with the given local ID.
+ *
+ * @param adbConnection AdbConnection that this stream is running on
+ * @param localId Local ID of the stream
+ */
+ AdbStream(AdbConnection adbConnection, int localId)
+ throws IOException, InterruptedException, AdbPairingRequiredException {
+ this.mAdbConnection = adbConnection;
+ this.mLocalId = localId;
+ this.mReadQueue = new ConcurrentLinkedQueue<>();
+ this.mReadBuffer = (ByteBuffer) ByteBuffer.allocate(adbConnection.getMaxData()).flip();
+ this.mWriteReady = new AtomicBoolean(false);
+ this.mIsClosed = false;
+ }
+
+ public AdbInputStream openInputStream() {
+ return new AdbInputStream(this);
+ }
+
+ public AdbOutputStream openOutputStream() {
+ return new AdbOutputStream(this);
+ }
+
+ /**
+ * Called by the connection thread to indicate newly received data.
+ *
+ * @param payload Data inside the WRTE message
+ */
+ void addPayload(byte[] payload) {
+ synchronized (mReadQueue) {
+ mReadQueue.add(payload);
+ mReadQueue.notifyAll();
+ }
+ }
+
+ /**
+ * Called by the connection thread to send an OKAY packet, allowing the
+ * other side to continue transmission.
+ *
+ * @throws IOException If the connection fails while sending the packet
+ */
+ void sendReady() throws IOException {
+ // Generate and send a OKAY packet
+ mAdbConnection.sendPacket(AdbProtocol.generateReady(mLocalId, mRemoteId));
+ }
+
+ /**
+ * Called by the connection thread to update the remote ID for this stream
+ *
+ * @param remoteId New remote ID
+ */
+ void updateRemoteId(int remoteId) {
+ this.mRemoteId = remoteId;
+ }
+
+ /**
+ * Called by the connection thread to indicate the stream is okay to send data.
+ */
+ void readyForWrite() {
+ mWriteReady.set(true);
+ }
+
+ /**
+ * Called by the connection thread to notify that the stream was closed by the peer.
+ */
+ void notifyClose(boolean closedByPeer) {
+ // We don't call close() because it sends another CLSE
+ if (closedByPeer && !mReadQueue.isEmpty()) {
+ // The remote peer closed the stream, but we haven't finished reading the remaining data
+ mPendingClose = true;
+ } else {
+ mIsClosed = true;
+ }
+
+ // Notify readers and writers
+ synchronized (this) {
+ notifyAll();
+ }
+ synchronized (mReadQueue) {
+ mReadQueue.notifyAll();
+ }
+ }
+
+ /**
+ * Read bytes from the ADB daemon.
+ *
+ * @return the next byte of data, or {@code -1} if the end of the stream is reached.
+ * @throws IOException If the stream fails while waiting
+ */
+ public int read(byte[] bytes, int offset, int length) throws IOException {
+ if (mReadBuffer.hasRemaining()) {
+ return readBuffer(bytes, offset, length);
+ }
+ // Buffer has no data, grab from the queue
+ synchronized (mReadQueue) {
+ byte[] data;
+ // Wait for the connection to close or data to be received
+ while ((data = mReadQueue.poll()) == null && !mIsClosed) {
+ try {
+ mReadQueue.wait();
+ } catch (InterruptedException e) {
+ //noinspection UnnecessaryInitCause
+ throw (IOException) new IOException().initCause(e);
+ }
+ }
+ // Add data to the buffer
+ if (data != null) {
+ mReadBuffer.clear();
+ mReadBuffer.put(data);
+ mReadBuffer.flip();
+ if (mReadBuffer.hasRemaining()) {
+ return readBuffer(bytes, offset, length);
+ }
+ }
+
+ if (mIsClosed) {
+ throw new IOException("Stream closed.");
+ }
+
+ if (mPendingClose && mReadQueue.isEmpty()) {
+ // The peer closed the stream, and we've finished reading the stream data, so this stream is finished
+ mIsClosed = true;
+ }
+ }
+
+ return -1;
+ }
+
+ private int readBuffer(byte[] bytes, int offset, int length) {
+ int count = 0;
+ for (int i = offset; i < offset + length; ++i) {
+ if (mReadBuffer.hasRemaining()) {
+ bytes[i] = mReadBuffer.get();
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Sends a WRTE packet with a given byte array payload. It does not flush the stream.
+ *
+ * @param bytes Payload in the form of a byte array
+ * @throws IOException If the stream fails while sending data
+ */
+ public void write(byte[] bytes, int offset, int length) throws IOException {
+ synchronized (this) {
+ // Make sure we're ready for a WRTE
+ while (!mIsClosed && !mWriteReady.compareAndSet(true, false)) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ //noinspection UnnecessaryInitCause
+ throw (IOException) new IOException().initCause(e);
+ }
+ }
+
+ if (mIsClosed) {
+ throw new IOException("Stream closed");
+ }
+ }
+ // Split and send data as WRTE packet
+ // TODO: A WRITE message may not be sent until a READY message is received.
+ // Once a WRITE message is sent, an additional WRITE message may not be
+ // sent until another READY message has been received. Recipients of
+ // a WRITE message that is in violation of this requirement will CLOSE
+ // the connection.
+ int maxData;
+ try {
+ maxData = mAdbConnection.getMaxData();
+ } catch (InterruptedException | AdbPairingRequiredException e) {
+ //noinspection UnnecessaryInitCause
+ throw (IOException) new IOException().initCause(e);
+ }
+ while (length != 0) {
+ if (length <= maxData) {
+ mAdbConnection.sendPacket(AdbProtocol.generateWrite(mLocalId, mRemoteId, bytes, offset, length));
+ offset = offset + length;
+ length = 0;
+ } else { // if (length > maxData) {
+ mAdbConnection.sendPacket(AdbProtocol.generateWrite(mLocalId, mRemoteId, bytes, offset, maxData));
+ offset = offset + maxData;
+ length = length - maxData;
+ }
+ }
+ }
+
+ public void flush() throws IOException {
+ if (mIsClosed) {
+ throw new IOException("Stream closed");
+ }
+ mAdbConnection.flushPacket();
+ }
+
+ /**
+ * Closes the stream. This sends a close message to the peer.
+ *
+ * @throws IOException If the stream fails while sending the close message.
+ */
+ @Override
+ public void close() throws IOException {
+ synchronized (this) {
+ // This may already be closed by the remote host
+ if (mIsClosed)
+ return;
+
+ // Notify readers/writers that we've closed
+ notifyClose(false);
+ }
+
+ mAdbConnection.sendPacket(AdbProtocol.generateClose(mLocalId, mRemoteId));
+ }
+
+ /**
+ * Returns whether the stream is closed or not
+ *
+ * @return True if the stream is close, false if not
+ */
+ public boolean isClosed() {
+ return mIsClosed;
+ }
+
+ /**
+ * Returns an estimate of available data.
+ *
+ * @return an estimate of the number of bytes that can be read from this stream without blocking.
+ * @throws IOException if the stream is close.
+ */
+ public int available() throws IOException {
+ synchronized (this) {
+ if (mIsClosed) {
+ throw new IOException("Stream closed.");
+ }
+ if (mReadBuffer.hasRemaining()) {
+ return mReadBuffer.remaining();
+ }
+ byte[] data = mReadQueue.peek();
+ return data == null ? 0 : data.length;
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AndroidPubkey.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AndroidPubkey.java
new file mode 100644
index 0000000..2e0496c
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AndroidPubkey.java
@@ -0,0 +1,245 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import android.util.Base64;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.RSAPublicKeySpec;
+import java.util.Objects;
+
+import javax.crypto.Cipher;
+
+final class AndroidPubkey {
+ /**
+ * Size of an RSA modulus such as an encrypted block or a signature.
+ */
+ public static final int ANDROID_PUBKEY_MODULUS_SIZE = 2048 / 8;
+
+ /**
+ * Size of an encoded RSA key.
+ */
+ public static final int ANDROID_PUBKEY_ENCODED_SIZE = 3 * 4 + 2 * ANDROID_PUBKEY_MODULUS_SIZE;
+
+ /**
+ * Size of the RSA modulus in words.
+ */
+ public static final int ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE / 4;
+
+ /**
+ * The RSA signature padding as an int array.
+ */
+ private static final int[] SIGNATURE_PADDING_AS_INT = new int[]{
+ 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00,
+ 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00,
+ 0x04, 0x14
+ };
+
+ /**
+ * The RSA signature padding as a byte array
+ */
+ private static final byte[] RSA_SHA_PKCS1_SIGNATURE_PADDING;
+
+ static {
+ RSA_SHA_PKCS1_SIGNATURE_PADDING = new byte[SIGNATURE_PADDING_AS_INT.length];
+
+ for (int i = 0; i < RSA_SHA_PKCS1_SIGNATURE_PADDING.length; i++)
+ RSA_SHA_PKCS1_SIGNATURE_PADDING[i] = (byte) SIGNATURE_PADDING_AS_INT[i];
+ }
+
+ /**
+ * Signs the ADB SHA1 payload with the private key of this object.
+ *
+ * @param privateKey Private key to sign with
+ * @param payload SHA1 payload to sign
+ * @return Signed SHA1 payload
+ * @throws GeneralSecurityException If signing fails
+ */
+ // Taken from adb_auth_sign
+ @NonNull
+ public static byte[] adbAuthSign(@NonNull PrivateKey privateKey, byte[] payload)
+ throws GeneralSecurityException {
+ Cipher c = Cipher.getInstance("RSA/ECB/NoPadding");
+ c.init(Cipher.ENCRYPT_MODE, privateKey);
+ c.update(RSA_SHA_PKCS1_SIGNATURE_PADDING);
+ return c.doFinal(payload);
+ }
+
+ /**
+ * Converts a standard RSAPublicKey object to the special ADB format. Available since 4.2.2.
+ *
+ * @param publicKey RSAPublicKey object to convert
+ * @param name Name without null terminator
+ * @return Byte array containing the converted RSAPublicKey object
+ */
+ @NonNull
+ public static byte[] encodeWithName(@NonNull RSAPublicKey publicKey, @NonNull String name)
+ throws InvalidKeyException {
+ int pkeySize = 4 * (int) Math.ceil(ANDROID_PUBKEY_ENCODED_SIZE / 3.0);
+ try (ByteArrayNoThrowOutputStream bos = new ByteArrayNoThrowOutputStream(pkeySize + name.length() + 2)) {
+ bos.write(Base64.encode(encode(publicKey),0));
+ bos.write(getUserInfo(name));
+ return bos.toByteArray();
+ }
+ }
+
+ // Taken from get_user_info except that a custom name is used instead of host@user
+ @VisibleForTesting
+ @NonNull
+ static byte[] getUserInfo(@NonNull String name) {
+ return StringCompat.getBytes(String.format(" %s\u0000", name), "UTF-8");
+ }
+
+ // https://android.googlesource.com/platform/system/core/+/e797a5c75afc17024d0f0f488c130128fcd704e2/libcrypto_utils/android_pubkey.cpp
+ // typedef struct RSAPublicKey {
+ // uint32_t modulus_size_words; // Modulus length. This must be ANDROID_PUBKEY_MODULUS_SIZE.
+ // uint32_t n0inv; // Precomputed montgomery parameter: -1 / n[0] mod 2^32
+ // uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE]; // RSA modulus as a little-endian array.
+ // uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE]; // Montgomery parameter R^2 as a little-endian array.
+ // uint32_t exponent; // RSA modulus: 3 or 65537
+ // } RSAPublicKey;
+
+ /**
+ * Allocates a new {@link RSAPublicKey} object, decodes a public RSA key stored in Android's custom binary format,
+ * and sets the key parameters. The resulting key can be used with the standard Java cryptography API to perform
+ * public operations.
+ *
+ * @param androidPubkey Public RSA key in Android's custom binary format. The size of the key must be at least
+ * {@link #ANDROID_PUBKEY_ENCODED_SIZE}
+ * @return {@link RSAPublicKey} object
+ */
+ @NonNull
+ public static RSAPublicKey decode(@NonNull byte[] androidPubkey)
+ throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException {
+ BigInteger n;
+ BigInteger e;
+
+ // Check size is large enough and the modulus size is correct.
+ if (androidPubkey.length < ANDROID_PUBKEY_ENCODED_SIZE) {
+ throw new InvalidKeyException("Invalid key length");
+ }
+ ByteBuffer keyStruct = ByteBuffer.wrap(androidPubkey).order(ByteOrder.LITTLE_ENDIAN);
+ int modulusSize = keyStruct.getInt();
+ if (modulusSize != ANDROID_PUBKEY_MODULUS_SIZE_WORDS) {
+ throw new InvalidKeyException("Invalid modulus length.");
+ }
+
+ // Convert the modulus to big-endian byte order as expected by BN_bin2bn.
+ byte[] modulus = new byte[ANDROID_PUBKEY_MODULUS_SIZE];
+ keyStruct.position(8);
+ keyStruct.get(modulus);
+ n = new BigInteger(1, swapEndianness(modulus));
+
+ // Read the exponent.
+ keyStruct.position(520);
+ e = BigInteger.valueOf(keyStruct.getInt());
+
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e);
+ return (RSAPublicKey) keyFactory.generatePublic(publicKeySpec);
+ }
+
+ /**
+ * Encodes the given key in the Android RSA public key binary format.
+ *
+ * @return Public RSA key in Android's custom binary format. The size of the key should be at least
+ * {@link #ANDROID_PUBKEY_ENCODED_SIZE}
+ */
+ @NonNull
+ public static byte[] encode(@NonNull RSAPublicKey publicKey) throws InvalidKeyException {
+ BigInteger r32;
+ BigInteger n0inv;
+ BigInteger rr;
+
+ if (publicKey.getModulus().toByteArray().length < ANDROID_PUBKEY_MODULUS_SIZE) {
+ throw new InvalidKeyException("Invalid key length " + publicKey.getModulus().toByteArray().length);
+ }
+
+ ByteBuffer keyStruct = ByteBuffer.allocate(ANDROID_PUBKEY_ENCODED_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+ // Store the modulus size.
+ keyStruct.putInt(ANDROID_PUBKEY_MODULUS_SIZE_WORDS); // modulus_size_words
+
+ // Compute and store n0inv = -1 / N[0] mod 2^32.
+ r32 = BigInteger.ZERO.setBit(32); // r32 = 2^32
+ n0inv = publicKey.getModulus().mod(r32); // n0inv = N[0] mod 2^32
+ n0inv = n0inv.modInverse(r32); // n0inv = 1/n0inv mod 2^32
+ n0inv = r32.subtract(n0inv); // n0inv = 2^32 - n0inv
+ keyStruct.putInt(n0inv.intValue()); // n0inv
+
+ // Store the modulus.
+ keyStruct.put(Objects.requireNonNull(BigEndianToLittleEndianPadded(ANDROID_PUBKEY_MODULUS_SIZE, publicKey.getModulus())));
+
+ // Compute and store rr = (2^(rsa_size)) ^ 2 mod N.
+ rr = BigInteger.ZERO.setBit(ANDROID_PUBKEY_MODULUS_SIZE * 8); // rr = 2^(rsa_size)
+ rr = rr.modPow(BigInteger.valueOf(2), publicKey.getModulus()); // rr = rr^2 mod N
+ keyStruct.put(Objects.requireNonNull(BigEndianToLittleEndianPadded(ANDROID_PUBKEY_MODULUS_SIZE, rr)));
+
+ // Store the exponent.
+ keyStruct.putInt(publicKey.getPublicExponent().intValue()); // exponent
+
+ return keyStruct.array();
+ }
+
+ @Nullable
+ private static byte[] BigEndianToLittleEndianPadded(int len, @NonNull BigInteger in) {
+ byte[] out = new byte[len];
+ byte[] bytes = swapEndianness(in.toByteArray()); // Convert big endian -> little endian
+ int num_bytes = bytes.length;
+ if (len < num_bytes) {
+ if (!fitsInBytes(bytes, num_bytes, len)) {
+ return null;
+ }
+ num_bytes = len;
+ }
+ System.arraycopy(bytes, 0, out, 0, num_bytes);
+ return out;
+ }
+
+ static boolean fitsInBytes(@NonNull byte[] bytes, int num_bytes, int len) {
+ byte mask = 0;
+ for (int i = len; i < num_bytes; i++) {
+ mask |= bytes[i];
+ }
+ return mask == 0;
+ }
+
+ @NonNull
+ private static byte[] swapEndianness(@NonNull byte[] bytes) {
+ int len = bytes.length;
+ byte[] out = new byte[len];
+ for (int i = 0; i < len; ++i) {
+ out[i] = bytes[len - i - 1];
+ }
+ return out;
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/ByteArrayNoThrowOutputStream.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/ByteArrayNoThrowOutputStream.java
new file mode 100644
index 0000000..030437a
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/ByteArrayNoThrowOutputStream.java
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import java.io.ByteArrayOutputStream;
+
+class ByteArrayNoThrowOutputStream extends ByteArrayOutputStream {
+ public ByteArrayNoThrowOutputStream() {
+ super();
+ }
+
+ public ByteArrayNoThrowOutputStream(int size) {
+ super(size);
+ }
+
+ @Override
+ public void write(byte[] b) {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ public void close() {
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/KeyPair.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/KeyPair.java
new file mode 100644
index 0000000..97a81c1
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/KeyPair.java
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.Certificate;
+
+import javax.security.auth.DestroyFailedException;
+
+final class KeyPair {
+ private final PrivateKey mPrivateKey;
+ private final Certificate mCertificate;
+
+ public KeyPair(PrivateKey privateKey, Certificate certificate) {
+ mPrivateKey = privateKey;
+ mCertificate = certificate;
+ }
+
+ public PrivateKey getPrivateKey() {
+ return mPrivateKey;
+ }
+
+ public PublicKey getPublicKey() {
+ return mCertificate.getPublicKey();
+ }
+
+ public Certificate getCertificate() {
+ return mCertificate;
+ }
+
+ public void destroy() throws DestroyFailedException {
+ try {
+ mPrivateKey.destroy();
+ } catch (NoSuchMethodError ignore) {
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/LocalServices.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/LocalServices.java
new file mode 100644
index 0000000..4dac75f
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/LocalServices.java
@@ -0,0 +1,271 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import android.text.TextUtils;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+
+/**
+ * Local services extracted from the ADB client
+ * for easy access.
+ */
+public class LocalServices {
+ static final int SERVICE_FIRST = 1;
+
+ public static final int SHELL = 1;
+ /**
+ * Remount the device's filesystem in read-write mode, instead of read-only. This is usually necessary before
+ * performing an {@link #SYNC} request. This request may not succeed on certain builds which do not allow that.
+ *
+ * This essentially executes {@code /system/bin/remount} command. Additional arguments such as {@code -R} can be
+ * passed too.
+ */
+ public static final int REMOUNT = 2;
+ public static final int FILE = 3;
+ public static final int TCP_CONNECT = 4;
+ public static final int LOCAL_UNIX_SOCKET = 5;
+ public static final int LOCAL_UNIX_SOCKET_RESERVED = 6;
+ public static final int LOCAL_UNIX_SOCKET_ABSTRACT = 7;
+ public static final int LOCAL_UNIX_SOCKET_FILE_SYSTEM = 8;
+ /**
+ * Receive snapshots of the framebuffer. It requires sufficient privileges (or the connection is closed immediately)
+ * but works as follows:
+ *
+ * After an {@link AdbStream} is opened, ADB daemon sends a 16-byte binary structure containing the following fields
+ * (little-endian format):
+ *
+ * After that, each time a snapshot is wanted, one byte should be sent through the channel, which will trigger the
+ * daemon to send {@code size} bytes of framebuffer data.
+ */
+ public static final int FRAMEBUFFER = 9;
+ /**
+ * Connects to the JDWP thread running in the VM of process PID (specified as an argument).
+ */
+ public static final int CONNECT_JDWP = 10;
+ /**
+ * Receive the list of JDWP PIDs periodically. The format of the returned data is the following (in order):
+ *
+ *
{@code hex4}: The length of all content as a 4-char hexadecimal string i.e. {@code %04zx}.
+ *
{@code content}: A series of ASCII lines of the following format:
+ *
+ * <pid> "\n"
+ *
+ *
+ * This service is used by DDMS to know which debuggable processes are running on the device/emulator.
+ *
+ * Note that there is no single-shot service to retrieve the list only once.
+ */
+ public static final int TRACK_JDWP = 11;
+ public static final int SYNC = 12;
+ /**
+ * Reverse socket connections from the device running ADB daemon to this client. This should not be used if both
+ * the ADB daemon and the client are in the same device.
+ *
+ * It takes an additional argument called {@code forward-command}. It can be one of the following:
+ *
+ *
{@code list-forward}: List all forwarded connections from the device
+ * This returns something that looks like the following:
+ *
+ *
{@code hex4}: The length of the payload, as 4 hexadecimal chars i.e. {@code %04zx}.
+ *
{@code payload}: A series of lines of the following format:
+ *
+ * host " " <local> " " <remote> "\n"
+ *
+ * Where <local> is the device-specific endpoint (e.g. {@code tcp:9000}), and <remote> is the
+ * client-specific endpoint.
+ *
+ *
forward:;
+ *
forward:norebind:;
+ *
killforward-all
+ *
killforward:
+ *
+ */
+ public static final int REVERSE = 13;
+ /**
+ * Backup some or all packages installed in the device. For this to work, {@code allowBackup=true} must be present
+ * in the application section of the AndroidManifest.xml of the app.
+ *
+ * It takes additional arguments which can be one of the following:
+ *
+ *
List of packages (as array)
+ *
{@code -all}
+ *
{@code -shared}
+ *
+ * Output is a stream which is in zlib format with 24 bytes at the front (if unencrypted).
+ */
+ public static final int BACKUP = 14;
+ /**
+ * Restore a backup. Input is a stream which is in zlib format with 24 bytes at the front (if unencrypted).
+ */
+ public static final int RESTORE = 15;
+
+ static final int SERVICE_LAST = 15;
+
+ @IntDef({
+ SHELL,
+ REMOUNT,
+ FILE,
+ TCP_CONNECT,
+ LOCAL_UNIX_SOCKET,
+ LOCAL_UNIX_SOCKET_RESERVED,
+ LOCAL_UNIX_SOCKET_ABSTRACT,
+ LOCAL_UNIX_SOCKET_FILE_SYSTEM,
+ FRAMEBUFFER,
+ CONNECT_JDWP,
+ TRACK_JDWP,
+ SYNC,
+ REVERSE,
+ BACKUP,
+ RESTORE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Services {
+ }
+
+ @NonNull
+ static String getServiceName(@Services int service) {
+ switch (service) {
+ case SHELL:
+ return "shell:";
+ case CONNECT_JDWP:
+ return "jdwp:";
+ case FILE:
+ return "dev:";
+ case FRAMEBUFFER:
+ return "framebuffer:";
+ case LOCAL_UNIX_SOCKET:
+ return "local:";
+ case LOCAL_UNIX_SOCKET_ABSTRACT:
+ return "localabstract:";
+ case LOCAL_UNIX_SOCKET_FILE_SYSTEM:
+ return "localfilesystem:";
+ case LOCAL_UNIX_SOCKET_RESERVED:
+ return "localreserved:";
+ case REMOUNT:
+ return "remount:";
+ case REVERSE:
+ return "reverse:";
+ case SYNC:
+ return "sync:";
+ case TCP_CONNECT:
+ return "tcp:";
+ case TRACK_JDWP:
+ return "track-jdwp";
+ case BACKUP:
+ return "backup:";
+ case RESTORE:
+ return "restore:";
+ default:
+ throw new IllegalArgumentException("Invalid service: " + service);
+ }
+ }
+
+ @NonNull
+ static String getDestination(@Services int service, @NonNull String... args) {
+ String serviceName = getServiceName(service);
+ StringBuilder destination = new StringBuilder(serviceName);
+ switch (service) {
+ case SHELL:
+ for (String arg : args) {
+ if (arg.contains("\"")) {
+ throw new IllegalArgumentException("Arguments for inline shell cannot contain double" +
+ " quotations.");
+ }
+ if (arg.contains(" ")) {
+ destination.append("\"").append(Objects.requireNonNull(arg)).append("\"");
+ } else destination.append(Objects.requireNonNull(arg));
+ }
+ break;
+ case FILE:
+ if (args.length == 0) {
+ throw new IllegalArgumentException("File name must be specified.");
+ } else if (args.length != 1) {
+ throw new IllegalArgumentException("Service expects exactly one argument, " + args.length
+ + " supplied.");
+ }
+ destination.append(Objects.requireNonNull(args[0]));
+ break;
+ case TCP_CONNECT:
+ if (args.length == 0) {
+ throw new IllegalArgumentException("Port number must be specified.");
+ } else if (args.length == 1) {
+ destination.append(args[0]);
+ } else if (args.length == 2) {
+ destination.append(Objects.requireNonNull(args[0]))
+ .append(':')
+ .append(Objects.requireNonNull(args[1]));
+ } else {
+ throw new IllegalArgumentException("Invalid number of arguments supplied.");
+ }
+ break;
+ case LOCAL_UNIX_SOCKET:
+ case LOCAL_UNIX_SOCKET_ABSTRACT:
+ case LOCAL_UNIX_SOCKET_FILE_SYSTEM:
+ case LOCAL_UNIX_SOCKET_RESERVED:
+ if (args.length == 0) {
+ throw new IllegalArgumentException("Path must be specified.");
+ } else if (args.length != 1) {
+ throw new IllegalArgumentException("Service expects exactly one argument, " + args.length
+ + " supplied.");
+ }
+ destination.append(Objects.requireNonNull(args[0]));
+ break;
+ case CONNECT_JDWP:
+ if (args.length == 0) {
+ throw new IllegalArgumentException("PID must be specified.");
+ } else if (args.length != 1) {
+ throw new IllegalArgumentException("Service expects exactly one argument, " + args.length
+ + " supplied.");
+ }
+ destination.append(Objects.requireNonNull(args[0]));
+ break;
+ case REVERSE:
+ if (args.length == 0) {
+ throw new IllegalArgumentException("Forward command must be specified.");
+ } else if (args.length != 1) {
+ throw new IllegalArgumentException("Service expects exactly one argument, " + args.length
+ + " supplied.");
+ }
+ if (args[0] == null) {
+ throw new IllegalArgumentException("Forward command is empty");
+ }
+ if ("list-forward".equals(args[0]) || "killforward-all".equals(args[0])) {
+ destination.append(args[0]);
+ } else if (args[0].startsWith("forward:") || args[0].startsWith("killforward:")) {
+ destination.append(args[0]);
+ } else {
+ throw new IllegalArgumentException("Invalid forward command.");
+ }
+ break;
+ case BACKUP:
+ if (args.length == 0) {
+ throw new IllegalArgumentException("At least one package must be specified or use -shared/-all.");
+ }
+ case REMOUNT:
+ // Additional arguments for the commands
+ destination.append(TextUtils.join(" ", args));
+ break;
+ case RESTORE:
+ case FRAMEBUFFER:
+ case SYNC:
+ case TRACK_JDWP:
+ if (args.length != 0) {
+ throw new IllegalArgumentException("Service expects no arguments.");
+ }
+ break;
+ }
+ return destination.toString();
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PRNGFixes.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PRNGFixes.java
new file mode 100644
index 0000000..e7fc0cf
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PRNGFixes.java
@@ -0,0 +1,319 @@
+// SPDX-License-Identifier: MIT AND (GPL-3.0-or-later OR Apache-2.0)
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.SecureRandomSpi;
+import java.security.Security;
+
+/**
+ * Fixes for the output of the default PRNG having low entropy.
+ *
+ * The fixes need to be applied via {@link #apply()} before any use of Java
+ * Cryptography Architecture primitives. A good place to invoke them is in the
+ * application's {@code onCreate}.
+ */
+// Copyright 2013 Google Inc.
+public final class PRNGFixes {
+ private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial();
+
+ /**
+ * Hidden constructor to prevent instantiation.
+ */
+ private PRNGFixes() {
+ }
+
+ /**
+ * Applies all fixes.
+ *
+ * @throws SecurityException if a fix is needed but could not be applied.
+ */
+ public static void apply() {
+ applyOpenSSLFix();
+ installLinuxPRNGSecureRandom();
+ }
+
+ /**
+ * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
+ * fix is not needed.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void applyOpenSSLFix() throws SecurityException {
+ if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
+ || (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2)) {
+ // No need to apply the fix
+ return;
+ }
+
+ try {
+ // Mix in the device- and invocation-specific seed.
+ Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_seed", byte[].class)
+ .invoke(null, generateSeed());
+
+ // Mix output of Linux PRNG into OpenSSL's PRNG
+ int bytesRead = (Integer) Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_load_file", String.class, long.class)
+ .invoke(null, "/dev/urandom", 1024);
+ if (bytesRead != 1024) {
+ throw new IOException("Unexpected number of bytes read from Linux PRNG: " + bytesRead);
+ }
+ } catch (Exception e) {
+ throw new SecurityException("Failed to seed OpenSSL PRNG", e);
+ }
+ }
+
+ /**
+ * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
+ * default. Does nothing if the implementation is already the default or if
+ * there is not need to install the implementation.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void installLinuxPRNGSecureRandom()
+ throws SecurityException {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ // No need to apply the fix
+ return;
+ }
+
+ // Install a Linux PRNG-based SecureRandom implementation as the
+ // default, if not yet installed.
+ Provider[] secureRandomProviders = Security.getProviders("SecureRandom.SHA1PRNG");
+ if ((secureRandomProviders == null)
+ || (secureRandomProviders.length < 1)
+ || (!LinuxPRNGSecureRandomProvider.class.equals(secureRandomProviders[0].getClass()))) {
+ Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
+ }
+
+ // Assert that new SecureRandom() and
+ // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
+ // by the Linux PRNG-based SecureRandom implementation.
+ SecureRandom rng1 = new SecureRandom();
+ if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider().getClass())) {
+ throw new SecurityException("new SecureRandom() backed by wrong Provider: "
+ + rng1.getProvider().getClass());
+ }
+
+ SecureRandom rng2;
+ try {
+ rng2 = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException("SHA1PRNG not available", e);
+ }
+ if (!LinuxPRNGSecureRandomProvider.class.equals(
+ rng2.getProvider().getClass())) {
+ throw new SecurityException("SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong provider: "
+ + rng2.getProvider().getClass());
+ }
+ }
+
+ /**
+ * {@code Provider} of {@code SecureRandom} engines which pass through
+ * all requests to the Linux PRNG.
+ */
+ private static class LinuxPRNGSecureRandomProvider extends Provider {
+
+ public LinuxPRNGSecureRandomProvider() {
+ super("LinuxPRNG", 1.0, "A Linux-specific random number provider that uses /dev/urandom");
+ // Although /dev/urandom is not a SHA-1 PRNG, some apps
+ // explicitly request a SHA1PRNG SecureRandom and we thus need to
+ // prevent them from getting the default implementation whose output
+ // may have low entropy.
+ put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
+ put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
+ }
+ }
+
+ /**
+ * {@link SecureRandomSpi} which passes all requests to the Linux PRNG
+ * ({@code /dev/urandom}).
+ */
+ public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
+
+ /*
+ * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
+ * are passed through to the Linux PRNG (/dev/urandom). Instances of
+ * this class seed themselves by mixing in the current time, PID, UID,
+ * build fingerprint, and hardware serial number (where available) into
+ * Linux PRNG.
+ *
+ * Concurrency: Read requests to the underlying Linux PRNG are
+ * serialized (on sLock) to ensure that multiple threads do not get
+ * duplicated PRNG output.
+ */
+
+ private static final File URANDOM_FILE = new File("/dev/urandom");
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Input stream for reading from Linux PRNG or {@code null} if not yet
+ * opened.
+ */
+ @GuardedBy("sLock")
+ private static DataInputStream sUrandomIn;
+
+ /**
+ * Output stream for writing to Linux PRNG or {@code null} if not yet
+ * opened.
+ */
+ @GuardedBy("sLock")
+ private static OutputStream sUrandomOut;
+
+ /**
+ * Whether this engine instance has been seeded. This is needed because
+ * each instance needs to seed itself if the client does not explicitly
+ * seed it.
+ */
+ private boolean mSeeded;
+
+ @Override
+ protected void engineSetSeed(byte[] bytes) {
+ try {
+ OutputStream out;
+ synchronized (sLock) {
+ out = getUrandomOutputStream();
+ }
+ out.write(bytes);
+ out.flush();
+ } catch (IOException e) {
+ // On a small fraction of devices /dev/urandom is not writable.
+ // Log and ignore.
+ Log.w(PRNGFixes.class.getSimpleName(),
+ "Failed to mix seed into " + URANDOM_FILE);
+ } finally {
+ mSeeded = true;
+ }
+ }
+
+ @Override
+ protected void engineNextBytes(byte[] bytes) {
+ if (!mSeeded) {
+ // Mix in the device- and invocation-specific seed.
+ engineSetSeed(generateSeed());
+ }
+
+ try {
+ DataInputStream in;
+ synchronized (sLock) {
+ in = getUrandomInputStream();
+ }
+ synchronized (in) {
+ in.readFully(bytes);
+ }
+ } catch (IOException e) {
+ throw new SecurityException(
+ "Failed to read from " + URANDOM_FILE, e);
+ }
+ }
+
+ @Override
+ protected byte[] engineGenerateSeed(int size) {
+ byte[] seed = new byte[size];
+ engineNextBytes(seed);
+ return seed;
+ }
+
+ private DataInputStream getUrandomInputStream() {
+ synchronized (sLock) {
+ if (sUrandomIn == null) {
+ // NOTE: Consider inserting a BufferedInputStream between
+ // DataInputStream and FileInputStream if you need higher
+ // PRNG output performance and can live with future PRNG
+ // output being pulled into this process prematurely.
+ try {
+ sUrandomIn = new DataInputStream(
+ new FileInputStream(URANDOM_FILE));
+ } catch (IOException e) {
+ throw new SecurityException("Failed to open "
+ + URANDOM_FILE + " for reading", e);
+ }
+ }
+ return sUrandomIn;
+ }
+ }
+
+ private OutputStream getUrandomOutputStream() throws IOException {
+ synchronized (sLock) {
+ if (sUrandomOut == null) {
+ sUrandomOut = new FileOutputStream(URANDOM_FILE);
+ }
+ return sUrandomOut;
+ }
+ }
+ }
+
+ /**
+ * Generates a device- and invocation-specific seed to be mixed into the
+ * Linux PRNG.
+ */
+ private static byte[] generateSeed() {
+ try {
+ ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
+ DataOutputStream seedBufferOut =
+ new DataOutputStream(seedBuffer);
+ seedBufferOut.writeLong(System.currentTimeMillis());
+ seedBufferOut.writeLong(System.nanoTime());
+ seedBufferOut.writeInt(Process.myPid());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BASE_1_1) {
+ seedBufferOut.writeInt(Process.myUid());
+ }
+ seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
+ seedBufferOut.close();
+ return seedBuffer.toByteArray();
+ } catch (IOException e) {
+ throw new SecurityException("Failed to generate seed", e);
+ }
+ }
+
+ /**
+ * Gets the hardware serial number of this device.
+ *
+ * @return serial number or {@code null} if not available.
+ */
+ private static String getDeviceSerialNumber() {
+ // We're using the Reflection API because Build.SERIAL is only available
+ // since API Level 9 (Gingerbread, Android 2.3).
+ try {
+ return (String) Build.class.getField("SERIAL").get(null);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private static byte[] getBuildFingerprintAndDeviceSerial() {
+ StringBuilder result = new StringBuilder();
+ String fingerprint = Build.FINGERPRINT;
+ if (fingerprint != null) {
+ result.append(fingerprint);
+ }
+ String serial = getDeviceSerialNumber();
+ if (serial != null) {
+ result.append(serial);
+ }
+ try {
+ return result.toString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not supported");
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingAuthCtx.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingAuthCtx.java
new file mode 100644
index 0000000..e1ea3f2
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingAuthCtx.java
@@ -0,0 +1,132 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+
+import org.bouncycastle.crypto.InvalidCipherTextException;
+import org.bouncycastle.crypto.digests.SHA256Digest;
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.modes.GCMModeCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.HKDFParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+import javax.security.auth.Destroyable;
+
+import io.github.muntashirakon.crypto.spake2.Spake2Context;
+import io.github.muntashirakon.crypto.spake2.Spake2Role;
+
+
+@RequiresApi(Build.VERSION_CODES.GINGERBREAD)
+class PairingAuthCtx implements Destroyable {
+ // The following values are taken from the following source and are subjected to change
+ // https://github.com/aosp-mirror/platform_system_core/blob/android-11.0.0_r1/adb/pairing_auth/pairing_auth.cpp
+ private static final byte[] CLIENT_NAME = StringCompat.getBytes("adb pair client\u0000", "UTF-8");
+ private static final byte[] SERVER_NAME = StringCompat.getBytes("adb pair server\u0000", "UTF-8");
+
+ // The following values are taken from the following source and are subjected to change
+ // https://github.com/aosp-mirror/platform_system_core/blob/android-11.0.0_r1/adb/pairing_auth/aes_128_gcm.cpp
+ private static final byte[] INFO = StringCompat.getBytes("adb pairing_auth aes-128-gcm key", "UTF-8");
+ private static final int HKDF_KEY_LENGTH = 128 / 8;
+ public static final int GCM_IV_LENGTH = 12; // in bytes
+
+ private final byte[] mMsg;
+ private final Spake2Context mSpake2Ctx;
+ private final byte[] mSecretKey = new byte[HKDF_KEY_LENGTH];
+ private long mDecIv = 0;
+ private long mEncIv = 0;
+ private boolean mIsDestroyed = false;
+
+ @Nullable
+ public static PairingAuthCtx createAlice(byte[] password) {
+ Spake2Context spake25519 = new Spake2Context(Spake2Role.Alice, CLIENT_NAME, SERVER_NAME);
+ try {
+ return new PairingAuthCtx(spake25519, password);
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ return null;
+ }
+ }
+
+ @VisibleForTesting
+ @Nullable
+ public static PairingAuthCtx createBob(byte[] password) {
+ Spake2Context spake25519 = new Spake2Context(Spake2Role.Bob, SERVER_NAME, CLIENT_NAME);
+ try {
+ return new PairingAuthCtx(spake25519, password);
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ return null;
+ }
+ }
+
+ private PairingAuthCtx(Spake2Context spake25519, byte[] password)
+ throws IllegalArgumentException, IllegalStateException {
+ mSpake2Ctx = spake25519;
+ mMsg = mSpake2Ctx.generateMessage(password);
+ }
+
+ public byte[] getMsg() {
+ return mMsg;
+ }
+
+ public boolean initCipher(byte[] theirMsg) throws IllegalArgumentException, IllegalStateException {
+ if (mIsDestroyed) return false;
+ byte[] keyMaterial = mSpake2Ctx.processMessage(theirMsg);
+ if (keyMaterial == null) return false;
+ HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA256Digest());
+ hkdf.init(new HKDFParameters(keyMaterial, null, INFO));
+ hkdf.generateBytes(mSecretKey, 0, mSecretKey.length);
+ return true;
+ }
+
+ @Nullable
+ public byte[] encrypt(@NonNull byte[] in) {
+ return encryptDecrypt(true, in, ByteBuffer.allocate(GCM_IV_LENGTH)
+ .order(ByteOrder.LITTLE_ENDIAN).putLong(mEncIv++).array());
+ }
+
+ @Nullable
+ public byte[] decrypt(@NonNull byte[] in) {
+ return encryptDecrypt(false, in, ByteBuffer.allocate(GCM_IV_LENGTH)
+ .order(ByteOrder.LITTLE_ENDIAN).putLong(mDecIv++).array());
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return mIsDestroyed;
+ }
+
+ @Override
+ public void destroy() {
+ mIsDestroyed = true;
+ Arrays.fill(mSecretKey, (byte) 0);
+ mSpake2Ctx.destroy();
+ }
+
+ @Nullable
+ private byte[] encryptDecrypt(boolean forEncryption, @NonNull byte[] in, @NonNull byte[] iv) {
+ if (mIsDestroyed) return null;
+ AEADParameters spec = new AEADParameters(new KeyParameter(mSecretKey), mSecretKey.length * 8, iv);
+ GCMModeCipher cipher = GCMBlockCipher.newInstance(AESEngine.newInstance());
+ cipher.init(forEncryption, spec);
+ byte[] out = new byte[cipher.getOutputSize(in.length)];
+ int newOffset = cipher.processBytes(in, 0, in.length, out, 0);
+ try {
+ cipher.doFinal(out, newOffset);
+ } catch (InvalidCipherTextException e) {
+ return null;
+ }
+ return out;
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingConnectionCtx.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingConnectionCtx.java
new file mode 100644
index 0000000..f04883d
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingConnectionCtx.java
@@ -0,0 +1,384 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+
+// https://github.com/aosp-mirror/platform_system_core/blob/android-11.0.0_r1/adb/pairing_connection/pairing_connection.cpp
+// Also based on Shizuku's implementation
+@RequiresApi(Build.VERSION_CODES.GINGERBREAD)
+public final class PairingConnectionCtx implements Closeable {
+ public static final String TAG = PairingConnectionCtx.class.getSimpleName();
+
+ public static final String EXPORTED_KEY_LABEL = "adb-label\u0000";
+ public static final int EXPORT_KEY_SIZE = 64;
+
+ private enum State {
+ Ready,
+ ExchangingMsgs,
+ ExchangingPeerInfo,
+ Stopped
+ }
+
+ enum Role {
+ Client,
+ Server,
+ }
+
+ private final String mHost;
+ private final int mPort;
+ private final byte[] mPswd;
+ private final PeerInfo mPeerInfo;
+ private final SSLContext mSslContext;
+ private final Role mRole = Role.Client;
+
+ private DataInputStream mInputStream;
+ private DataOutputStream mOutputStream;
+ private PairingAuthCtx mPairingAuthCtx;
+ private State mState = State.Ready;
+
+ public PairingConnectionCtx(@NonNull String host, int port, @NonNull byte[] pswd, @NonNull KeyPair keyPair,
+ @NonNull String deviceName)
+ throws NoSuchAlgorithmException, KeyManagementException, InvalidKeyException {
+ this.mHost = Objects.requireNonNull(host);
+ this.mPort = port;
+ this.mPswd = Objects.requireNonNull(pswd);
+ this.mPeerInfo = new PeerInfo(PeerInfo.ADB_RSA_PUB_KEY, AndroidPubkey.encodeWithName((RSAPublicKey)
+ keyPair.getPublicKey(), Objects.requireNonNull(deviceName)));
+ this.mSslContext = SslUtils.getSslContext(keyPair);
+ }
+
+ public PairingConnectionCtx(@NonNull String host, int port, @NonNull byte[] pswd, @NonNull PrivateKey privateKey,
+ @NonNull Certificate certificate, @NonNull String deviceName)
+ throws NoSuchAlgorithmException, KeyManagementException, InvalidKeyException {
+ this(host, port, pswd, new KeyPair(Objects.requireNonNull(privateKey), Objects.requireNonNull(certificate)),
+ deviceName);
+ }
+
+ public void start() throws IOException {
+ if (mState != State.Ready) {
+ throw new IOException("Connection is not ready yet.");
+ }
+
+ mState = State.ExchangingMsgs;
+
+ // Start worker
+ setupTlsConnection();
+
+ for (; ; ) {
+ switch (mState) {
+ case ExchangingMsgs:
+ if (!doExchangeMsgs()) {
+ notifyResult();
+ throw new IOException("Exchanging message wasn't successful.");
+ }
+ mState = State.ExchangingPeerInfo;
+ break;
+ case ExchangingPeerInfo:
+ if (!doExchangePeerInfo()) {
+ notifyResult();
+ throw new IOException("Could not exchange peer info.");
+ }
+ notifyResult();
+ return;
+ case Ready:
+ case Stopped:
+ throw new IOException("Connection closed with errors.");
+ }
+ }
+ }
+
+ private void notifyResult() {
+ mState = State.Stopped;
+ }
+
+ private void setupTlsConnection() throws IOException {
+ Socket socket;
+ if (mRole == Role.Server) {
+ SSLServerSocket sslServerSocket = (SSLServerSocket) mSslContext.getServerSocketFactory().createServerSocket(mPort);
+ socket = sslServerSocket.accept();
+ // TODO: Write automated test scripts after removing Conscrypt dependency.
+ } else { // role == Role.Client
+ socket = new Socket(mHost, mPort);
+ }
+ socket.setTcpNoDelay(true);
+
+ // We use custom SSLContext to allow any SSL certificates
+ SSLSocket sslSocket = (SSLSocket) mSslContext.getSocketFactory().createSocket(socket, mHost, mPort, true);
+ sslSocket.startHandshake();
+ Log.d(TAG, "Handshake succeeded.");
+
+ mInputStream = new DataInputStream(sslSocket.getInputStream());
+ mOutputStream = new DataOutputStream(sslSocket.getOutputStream());
+
+ // To ensure the connection is not stolen while we do the PAKE, append the exported key material from the
+ // tls connection to the password.
+ byte[] keyMaterial = exportKeyingMaterial(sslSocket, EXPORT_KEY_SIZE);
+ byte[] passwordBytes = new byte[mPswd.length + keyMaterial.length];
+ System.arraycopy(mPswd, 0, passwordBytes, 0, mPswd.length);
+ System.arraycopy(keyMaterial, 0, passwordBytes, mPswd.length, keyMaterial.length);
+
+ PairingAuthCtx pairingAuthCtx = PairingAuthCtx.createAlice(passwordBytes);
+ if (pairingAuthCtx == null) {
+ throw new IOException("Unable to create PairingAuthCtx.");
+ }
+ this.mPairingAuthCtx = pairingAuthCtx;
+ }
+
+ @SuppressLint("PrivateApi") // Conscrypt is a stable private API
+ private byte[] exportKeyingMaterial(SSLSocket sslSocket, int length) throws SSLException {
+ // Conscrypt#exportKeyingMaterial(SSLSocket socket, String label, byte[] context, int length): byte[]
+ // throws SSLException
+ try {
+ Class> conscryptClass;
+ if (SslUtils.isCustomConscrypt()) {
+ conscryptClass = Class.forName("org.conscrypt.Conscrypt");
+ } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ // Although support for conscrypt has been added in Android 5.0 (Lollipop),
+ // TLS1.3 isn't supported until Android 9 (Pie).
+ throw new SSLException("TLSv1.3 isn't supported on your platform. Use custom Conscrypt library instead.");
+ } else {
+ conscryptClass = Class.forName("com.android.org.conscrypt.Conscrypt");
+ }
+ Method exportKeyingMaterial = conscryptClass.getMethod("exportKeyingMaterial", SSLSocket.class,
+ String.class, byte[].class, int.class);
+ return (byte[]) exportKeyingMaterial.invoke(null, sslSocket, EXPORTED_KEY_LABEL, null, length);
+ } catch (SSLException e) {
+ throw e;
+ } catch (Throwable th) {
+ throw new SSLException(th);
+ }
+ }
+
+ private void writeHeader(@NonNull PairingPacketHeader header, @NonNull byte[] payload) throws IOException {
+ ByteBuffer buffer = ByteBuffer.allocate(PairingPacketHeader.PAIRING_PACKET_HEADER_SIZE)
+ .order(ByteOrder.BIG_ENDIAN);
+ header.writeTo(buffer);
+
+ mOutputStream.write(buffer.array());
+ mOutputStream.write(payload);
+ }
+
+ @Nullable
+ private PairingPacketHeader readHeader() throws IOException {
+ byte[] bytes = new byte[PairingPacketHeader.PAIRING_PACKET_HEADER_SIZE];
+ mInputStream.readFully(bytes);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+ return PairingPacketHeader.readFrom(buffer);
+ }
+
+ @NonNull
+ private PairingPacketHeader createHeader(byte type, int payloadSize) {
+ return new PairingPacketHeader(PairingPacketHeader.CURRENT_KEY_HEADER_VERSION, type, payloadSize);
+ }
+
+ private boolean checkHeaderType(byte expected, byte actual) {
+ if (expected != actual) {
+ Log.e(TAG, "Unexpected header type (expected=" + expected + " actual=" + actual + ")");
+ return false;
+ }
+ return true;
+ }
+
+ private boolean doExchangeMsgs() throws IOException {
+ byte[] msg = mPairingAuthCtx.getMsg();
+
+ PairingPacketHeader ourHeader = createHeader(PairingPacketHeader.SPAKE2_MSG, msg.length);
+ // Write our SPAKE2 msg
+ writeHeader(ourHeader, msg);
+
+ // Read the peer's SPAKE2 msg header
+ PairingPacketHeader theirHeader = readHeader();
+ if (theirHeader == null || !checkHeaderType(PairingPacketHeader.SPAKE2_MSG, theirHeader.type)) return false;
+
+ // Read the SPAKE2 msg payload and initialize the cipher for encrypting the PeerInfo and certificate.
+ byte[] theirMsg = new byte[theirHeader.payloadSize];
+ mInputStream.readFully(theirMsg);
+
+ try {
+ return mPairingAuthCtx.initCipher(theirMsg);
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to initialize pairing cipher");
+ //noinspection UnnecessaryInitCause
+ throw (IOException) new IOException().initCause(e);
+ }
+ }
+
+ private boolean doExchangePeerInfo() throws IOException {
+ // Encrypt PeerInfo
+ ByteBuffer buffer = ByteBuffer.allocate(PeerInfo.MAX_PEER_INFO_SIZE).order(ByteOrder.BIG_ENDIAN);
+ mPeerInfo.writeTo(buffer);
+ byte[] outBuffer = mPairingAuthCtx.encrypt(buffer.array());
+ if (outBuffer == null) {
+ Log.e(TAG, "Failed to encrypt peer info");
+ return false;
+ }
+
+ // Write out the packet header
+ PairingPacketHeader ourHeader = createHeader(PairingPacketHeader.PEER_INFO, outBuffer.length);
+ // Write out the encrypted payload
+ writeHeader(ourHeader, outBuffer);
+
+ // Read in the peer's packet header
+ PairingPacketHeader theirHeader = readHeader();
+ if (theirHeader == null || !checkHeaderType(PairingPacketHeader.PEER_INFO, theirHeader.type)) return false;
+
+ // Read in the encrypted peer certificate
+ byte[] theirMsg = new byte[theirHeader.payloadSize];
+ mInputStream.readFully(theirMsg);
+
+ // Try to decrypt the certificate
+ byte[] decryptedMsg = mPairingAuthCtx.decrypt(theirMsg);
+ if (decryptedMsg == null) {
+ Log.e(TAG, "Unsupported payload while decrypting peer info.");
+ return false;
+ }
+
+ // The decrypted message should contain the PeerInfo.
+ if (decryptedMsg.length != PeerInfo.MAX_PEER_INFO_SIZE) {
+ Log.e(TAG, "Got size=" + decryptedMsg.length + " PeerInfo.size=" + PeerInfo.MAX_PEER_INFO_SIZE);
+ return false;
+ }
+
+ PeerInfo theirPeerInfo = PeerInfo.readFrom(ByteBuffer.wrap(decryptedMsg));
+ Log.d(TAG, theirPeerInfo.toString());
+ return true;
+ }
+
+ @Override
+ public void close() {
+ Arrays.fill(mPswd, (byte) 0);
+ try {
+ mInputStream.close();
+ } catch (IOException ignore) {
+ }
+ try {
+ mOutputStream.close();
+ } catch (IOException ignore) {
+ }
+ if (mState != State.Ready) {
+ mPairingAuthCtx.destroy();
+ }
+ }
+
+ private static class PeerInfo {
+ public static final int MAX_PEER_INFO_SIZE = 1 << 13;
+
+ public static final byte ADB_RSA_PUB_KEY = 0;
+ public static final byte ADB_DEVICE_GUID = 0;
+
+ @NonNull
+ public static PeerInfo readFrom(@NonNull ByteBuffer buffer) {
+ byte type = buffer.get();
+ byte[] data = new byte[MAX_PEER_INFO_SIZE - 1];
+ buffer.get(data);
+ return new PeerInfo(type, data);
+ }
+
+ private final byte type;
+ private final byte[] data = new byte[MAX_PEER_INFO_SIZE - 1];
+
+ public PeerInfo(byte type, byte[] data) {
+ this.type = type;
+ System.arraycopy(data, 0, this.data, 0, Math.min(data.length, MAX_PEER_INFO_SIZE - 1));
+ }
+
+ public void writeTo(@NonNull ByteBuffer buffer) {
+ buffer.put(type).put(data);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "PeerInfo{" +
+ "type=" + type +
+ ", data=" + Arrays.toString(data) +
+ '}';
+ }
+ }
+
+ private static class PairingPacketHeader {
+ public static final byte CURRENT_KEY_HEADER_VERSION = 1;
+ public static final byte MIN_SUPPORTED_KEY_HEADER_VERSION = 1;
+ public static final byte MAX_SUPPORTED_KEY_HEADER_VERSION = 1;
+
+ public static final int MAX_PAYLOAD_SIZE = 2 * PeerInfo.MAX_PEER_INFO_SIZE;
+ public static final byte PAIRING_PACKET_HEADER_SIZE = 6;
+
+ public static final byte SPAKE2_MSG = 0;
+ public static final byte PEER_INFO = 1;
+
+ @Nullable
+ public static PairingPacketHeader readFrom(@NonNull ByteBuffer buffer) {
+ byte version = buffer.get();
+ byte type = buffer.get();
+ int payload = buffer.getInt();
+ if (version < MIN_SUPPORTED_KEY_HEADER_VERSION || version > MAX_SUPPORTED_KEY_HEADER_VERSION) {
+ Log.e(TAG, "PairingPacketHeader version mismatch (us=" + CURRENT_KEY_HEADER_VERSION
+ + " them=" + version + ")");
+ return null;
+ }
+ if (type != SPAKE2_MSG && type != PEER_INFO) {
+ Log.e(TAG, "Unknown PairingPacket type " + type);
+ return null;
+ }
+ if (payload <= 0 || payload > MAX_PAYLOAD_SIZE) {
+ Log.e(TAG, "Header payload not within a safe payload size (size=" + payload + ")");
+ return null;
+ }
+ return new PairingPacketHeader(version, type, payload);
+ }
+
+ private final byte version;
+ private final byte type;
+ private final int payloadSize;
+
+ public PairingPacketHeader(byte version, byte type, int payloadSize) {
+ this.version = version;
+ this.type = type;
+ this.payloadSize = payloadSize;
+ }
+
+ public void writeTo(@NonNull ByteBuffer buffer) {
+ buffer.put(version).put(type).putInt(payloadSize);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "PairingPacketHeader{" +
+ "version=" + version +
+ ", type=" + type +
+ ", payloadSize=" + payloadSize +
+ '}';
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/SslUtils.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/SslUtils.java
new file mode 100644
index 0000000..77f4dcb
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/SslUtils.java
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+
+import java.net.Socket;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509TrustManager;
+
+final class SslUtils {
+ private static boolean customConscrypt = false;
+ private static SSLContext sslContext;
+
+ public static boolean isCustomConscrypt() {
+ return customConscrypt;
+ }
+
+ @SuppressLint("TrulyRandom") // The users are already instructed to fix this issue
+ @NonNull
+ public static SSLContext getSslContext(KeyPair keyPair) throws NoSuchAlgorithmException, KeyManagementException {
+ if (sslContext != null) {
+ return sslContext;
+ }
+ try {
+ Class> providerClass = Class.forName("org.conscrypt.OpenSSLProvider");
+ Provider openSslProvder = (Provider) providerClass.getDeclaredConstructor().newInstance();
+ sslContext = SSLContext.getInstance("TLSv1.3", openSslProvder);
+ customConscrypt = true;
+ } catch (NoSuchAlgorithmException e) {
+ throw e;
+ } catch (Throwable e) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ // Custom error message to inform user that they should use custom Conscrypt library.
+ throw new NoSuchAlgorithmException("TLSv1.3 isn't supported on your platform. Use custom Conscrypt library instead.");
+ }
+ sslContext = SSLContext.getInstance("TLSv1.3");
+ customConscrypt = false;
+ }
+ System.out.println("Using " + (customConscrypt ? "custom" : "default") + " TLSv1.3 provider...");
+ sslContext.init(new KeyManager[]{getKeyManager(keyPair)},
+ new X509TrustManager[]{getAllAcceptingTrustManager()},
+ new SecureRandom());
+ return sslContext;
+ }
+
+ @NonNull
+ private static KeyManager getKeyManager(KeyPair keyPair) {
+ return new X509ExtendedKeyManager() {
+ private final String mAlias = "key";
+
+ @Override
+ public String[] getClientAliases(String keyType, Principal[] issuers) {
+ return null;
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
+ for (String keyType : keyTypes) {
+ if (keyType.equals("RSA")) return mAlias;
+ }
+ return null;
+ }
+
+ @Override
+ public String[] getServerAliases(String keyType, Principal[] issuers) {
+ return null;
+ }
+
+ @Override
+ public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+ return null;
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ if (this.mAlias.equals(alias)) {
+ return new X509Certificate[]{(X509Certificate) keyPair.getCertificate()};
+ }
+ return null;
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ if (this.mAlias.equals(alias)) {
+ return keyPair.getPrivateKey();
+ }
+ return null;
+ }
+ };
+ }
+
+ @SuppressLint("TrustAllX509TrustManager") // Accept all certificates
+ @NonNull
+ private static X509TrustManager getAllAcceptingTrustManager() {
+ return new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ };
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/StringCompat.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/StringCompat.java
new file mode 100644
index 0000000..a51d8c4
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/StringCompat.java
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+
+final class StringCompat {
+ @NonNull
+ public static byte[] getBytes(@NonNull String text, @NonNull String charsetName) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ return text.getBytes(Charset.forName(charsetName));
+ }
+ try {
+ return text.getBytes(charsetName);
+ } catch (UnsupportedEncodingException e) {
+ throw (IllegalCharsetNameException) new IllegalCharsetNameException("Illegal charset " + charsetName)
+ .initCause(e);
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AdbMdns.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AdbMdns.java
new file mode 100644
index 0000000..a630c89
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AdbMdns.java
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.android;
+
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.StringDef;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.NetworkInterface;
+import java.net.ServerSocket;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * Automatic discovery of ADB daemons.
+ */
+// Copyright 2020 南宫雪珊
+// Copyright 2022 Muntashir Al-Islam
+// Based on https://android.googlesource.com/platform/packages/modules/adb/+/eddd2d3a386a83f5d1e14f87a318adef4c2f1a9d/adb_mdns.cpp
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+public class AdbMdns {
+ public static final String SERVICE_TYPE_ADB = "adb";
+ public static final String SERVICE_TYPE_TLS_PAIRING = "adb-tls-pairing";
+ public static final String SERVICE_TYPE_TLS_CONNECT = "adb-tls-connect";
+
+ @StringDef({
+ SERVICE_TYPE_ADB,
+ SERVICE_TYPE_TLS_PAIRING,
+ SERVICE_TYPE_TLS_CONNECT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ServiceType {
+ }
+
+ public interface OnAdbDaemonDiscoveredListener {
+ void onPortChanged(@Nullable InetAddress hostAddress, int port);
+ }
+
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final String mServiceType;
+ @NonNull
+ private final OnAdbDaemonDiscoveredListener mAdbDaemonDiscoveredListener;
+ private final NsdManager.DiscoveryListener mDiscoveryListener;
+ private final NsdManager mNsdManager;
+
+ private boolean mRegistered;
+ private boolean mRunning;
+ @Nullable
+ private String mServiceName;
+
+ public AdbMdns(@NonNull Context context, @ServiceType @NonNull String serviceType,
+ @NonNull OnAdbDaemonDiscoveredListener portChangeListener) {
+ mContext = Objects.requireNonNull(context);
+ mServiceType = String.format("_%s._tcp", Objects.requireNonNull(serviceType));
+ mAdbDaemonDiscoveredListener = Objects.requireNonNull(portChangeListener);
+ mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
+ mDiscoveryListener = new DiscoveryListener(this);
+ }
+
+ public void start() {
+ if (mRunning) return;
+ mRunning = true;
+ if (!mRegistered) {
+ mNsdManager.discoverServices(mServiceType, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
+ }
+ }
+
+ public void stop() {
+ if (!mRunning) return;
+ mRunning = false;
+ if (mRegistered) {
+ mNsdManager.stopServiceDiscovery(mDiscoveryListener);
+ }
+ }
+
+ private void onDiscoveryStart() {
+ mRegistered = true;
+ }
+
+ private void onDiscoverStop() {
+ mRegistered = false;
+ }
+
+ private void onServiceFound(NsdServiceInfo serviceInfo) {
+ mNsdManager.resolveService(serviceInfo, new ResolveListener(this));
+ }
+
+ private void onServiceLost(NsdServiceInfo serviceInfo) {
+ if (mServiceName != null && mServiceName.equals(serviceInfo.getServiceName())) {
+ mAdbDaemonDiscoveredListener.onPortChanged(serviceInfo.getHost(), -1);
+ }
+ }
+
+ private void onServiceResolved(NsdServiceInfo serviceInfo) {
+ if (!mRunning) return;
+ try {
+ for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
+ for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) {
+ String inetHost = inetAddress.getHostAddress();
+ if (inetHost != null && inetHost.equals(serviceInfo.getHost().getHostAddress())
+ && isPortAvailable(serviceInfo.getPort())) {
+ mServiceName = serviceInfo.getServiceName();
+ mAdbDaemonDiscoveredListener.onPortChanged(serviceInfo.getHost(), serviceInfo.getPort());
+ }
+ }
+ }
+ } catch (SocketException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private boolean isPortAvailable(int port) {
+ try (ServerSocket socket = new ServerSocket()) {
+ socket.bind(new InetSocketAddress(AndroidUtils.getHostIpAddress(mContext), port), 1);
+ return false;
+ } catch (IOException e) {
+ return true;
+ }
+ }
+
+ private static class DiscoveryListener implements NsdManager.DiscoveryListener {
+ @NonNull
+ private final AdbMdns mAdbMdns;
+
+ private DiscoveryListener(@NonNull AdbMdns adbMdns) {
+ mAdbMdns = adbMdns;
+ }
+
+ @Override
+ public void onDiscoveryStarted(String serviceType) {
+ mAdbMdns.onDiscoveryStart();
+ }
+
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+ }
+
+ @Override
+ public void onDiscoveryStopped(String serviceType) {
+ mAdbMdns.onDiscoverStop();
+ }
+
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+ }
+
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {
+ mAdbMdns.onServiceFound(serviceInfo);
+ }
+
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {
+ mAdbMdns.onServiceLost(serviceInfo);
+ }
+ }
+
+ private static class ResolveListener implements NsdManager.ResolveListener {
+ @NonNull
+ private final AdbMdns mAdbMdns;
+
+ private ResolveListener(@NonNull AdbMdns adbMdns) {
+ mAdbMdns = adbMdns;
+ }
+
+ @Override
+ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ }
+
+ @Override
+ public void onServiceResolved(NsdServiceInfo serviceInfo) {
+ mAdbMdns.onServiceResolved(serviceInfo);
+ }
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AndroidUtils.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AndroidUtils.java
new file mode 100644
index 0000000..8816934
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AndroidUtils.java
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0
+
+package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.android;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Build;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+public class AndroidUtils {
+ // https://github.com/firebase/firebase-android-sdk/blob/7d86138304a6573cbe2c61b66b247e930fa05767/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java#L402
+ private static final String GOLDFISH = "goldfish";
+ private static final String RANCHU = "ranchu";
+ private static final String SDK = "sdk";
+
+ public static boolean isEmulator(@NonNull Context context) {
+ if (Build.PRODUCT.contains(SDK)) {
+ return true;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO
+ && (Build.HARDWARE.contains(GOLDFISH) || Build.HARDWARE.contains(RANCHU))) {
+ return true;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
+ @SuppressLint("HardwareIds")
+ String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
+ return androidId == null;
+ }
+ return false;
+ }
+
+
+ @NonNull
+ public static String getHostIpAddress(@NonNull Context context) {
+ if (AndroidUtils.isEmulator(context)) {
+ return "10.0.2.2";
+ }
+ String ipAddress;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ ipAddress = InetAddress.getLoopbackAddress().getHostAddress();
+ } else {
+ try {
+ ipAddress = InetAddress.getLocalHost().getHostAddress();
+ } catch (UnknownHostException e) {
+ ipAddress = null;
+ }
+ }
+ if (ipAddress == null || ipAddress.equals("::1")) {
+ return "127.0.0.1";
+ }
+ return ipAddress;
+ }
+}
diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/package.html b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/package.html
new file mode 100644
index 0000000..704dd26
--- /dev/null
+++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/package.html
@@ -0,0 +1 @@
+
All Android dependencies are kept under this package for easy reference.