From 890539cbc04ee9431b34de1187f58e5a679b1240 Mon Sep 17 00:00:00 2001 From: Administrator Date: Fri, 4 Jul 2025 10:38:52 +0800 Subject: [PATCH] init --- .gitignore | 15 + .idea/.gitignore | 3 + .idea/AndroidProjectSystem.xml | 6 + .idea/compiler.xml | 6 + .idea/dbnavigator.xml | 429 +++ .idea/deploymentTargetSelector.xml | 10 + .idea/gradle.xml | 18 + .idea/migrations.xml | 10 + .idea/misc.xml | 10 + .idea/runConfigurations.xml | 17 + app/.gitignore | 1 + app/build.gradle.kts | 60 + app/proguard-rules.pro | 21 + .../android/grape/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 124 + app/src/main/aidl/android/app/IMikRom.aidl | 12 + .../java/com/android/grape/MainActivity.kt | 133 + .../java/com/android/grape/MainApplication.kt | 20 + .../java/com/android/grape/MainViewModel.kt | 26 + .../java/com/android/grape/data/AfInfo.kt | 19 + .../java/com/android/grape/data/BigoInfo.kt | 21 + .../java/com/android/grape/data/DeviceInfo.kt | 25 + .../com/android/grape/job/AutoJobService.kt | 44 + .../android/grape/job/CheckIpJobService.kt | 70 + .../grape/job/DownloadAppJobService.kt | 51 + .../com/android/grape/job/InstallService.kt | 67 + .../com/android/grape/job/MonitorService.kt | 66 + .../com/android/grape/job/OpenAppService.kt | 61 + .../android/grape/job/RecoverJobService.kt | 52 + .../grape/job/SendCallbackJobService.kt | 293 ++ .../grape/job/StartVpnPortJobService.kt | 45 + .../grape/job/StartVpnServerJobService.kt | 60 + .../com/android/grape/job/UnInstallService.kt | 36 + .../java/com/android/grape/net/HttpUtils.kt | 267 ++ .../main/java/com/android/grape/net/MyGet.kt | 241 ++ .../main/java/com/android/grape/net/MyPost.kt | 127 + .../com/android/grape/net/NetworkHelper.kt | 5 + .../java/com/android/grape/pseudo/Base64.kt | 819 +++++ .../com/android/grape/pseudo/Constants.kt | 8 + .../android/grape/pseudo/ManifestBuilder.kt | 71 + .../android/grape/pseudo/PseudoApkSigner.kt | 114 + .../grape/pseudo/SignatureFileGenerator.kt | 47 + .../java/com/android/grape/pseudo/Utils.kt | 84 + .../grape/pseudo/ZipAlignZipOutputStream.kt | 64 + .../android/grape/receiver/ScriptReceiver.kt | 40 + .../com/android/grape/sai/ApkSourceBuilder.kt | 142 + .../grape/sai/BaseSaiPackageInstaller.kt | 77 + .../grape/sai/FlexSaiPackageInstaller.kt | 112 + .../java/com/android/grape/sai/IOUtils.kt | 179 + .../android/grape/sai/MyBroadcastReceiver.kt | 56 + .../grape/sai/RootedSaiPackageInstaller.kt | 41 + .../sai/RootlessSaiPiBroadcastReceiver.kt | 188 + .../java/com/android/grape/sai/SafUtils.kt | 100 + .../grape/sai/ShellSaiPackageInstaller.kt | 389 ++ .../grape/sai/ShizukuSaiPackageInstaller.kt | 41 + .../java/com/android/grape/sai/Stopwatch.kt | 9 + .../main/java/com/android/grape/sai/Utils.kt | 243 ++ .../sai/apksource/CopyToFileApkSource.kt | 84 + .../grape/sai/apksource/FilterApkSource.kt | 56 + .../grape/sai/apksource/SignerApkSource.kt | 108 + .../ContentUriFileDescriptor.kt | 40 + .../sai/filedescriptor/DefaultApkSource.kt | 44 + .../sai/filedescriptor/FileDescriptor.kt | 15 + .../grape/sai/filedescriptor/FileUtils.kt | 52 + .../filedescriptor/NormalFileDescriptor.kt | 21 + .../grape/sai/filedescriptor/ZipApkSource.kt | 124 + .../sai/filedescriptor/ZipBackedApkSource.kt | 15 + .../sai/filedescriptor/ZipFileApkSource.kt | 110 + .../com/android/grape/sai/inter/ApkSource.kt | 32 + .../grape/sai/inter/SaiPackageInstaller.kt | 17 + .../grape/sai/inter/SaiPiSessionObserver.kt | 7 + .../android/grape/sai/param/PackageMeta.kt | 154 + .../grape/sai/param/SaiPiSessionParams.kt | 12 + .../grape/sai/param/SaiPiSessionState.kt | 143 + .../grape/sai/param/SaiPiSessionStatus.kt | 18 + .../grape/sai/prefers/DbgPreferencesHelper.kt | 32 + .../grape/sai/prefers/PreferencesHelper.kt | 145 + .../grape/sai/prefers/PreferencesKeys.kt | 31 + .../grape/sai/prefers/PreferencesValues.kt | 9 + .../rootless/AndroidPackageInstallerError.kt | 270 ++ .../ConfirmationIntentWrapperActivity2.kt | 78 + .../rootless/RootlessSaiPackageInstaller.kt | 174 + .../com/android/grape/sai/shell/MiuiUtils.kt | 85 + .../java/com/android/grape/sai/shell/Shell.kt | 78 + .../android/grape/sai/shell/ShizukuShell.kt | 110 + .../com/android/grape/sai/shell/SuShell.kt | 73 + .../grape/service/MyAccessibilityService.kt | 99 + .../android/grape/util/AdvertisingIdClient.kt | 119 + .../grape/util/ChangeDeviceInfoUtil.kt | 435 +++ .../java/com/android/grape/util/ClashUtil.kt | 161 + .../android/grape/util/DeviceConvertUtil.kt | 247 ++ .../java/com/android/grape/util/FileUtils.kt | 390 ++ .../java/com/android/grape/util/MockTools.kt | 67 + .../android/grape/util/ReflectionHelper.kt | 61 + .../java/com/android/grape/util/ScriptUtil.kt | 25 + .../com/android/grape/util/ServiceUtils.kt | 114 + .../com/android/grape/util/ShellUtils.java | 382 ++ .../grape/util/StoragePermissionHelper.kt | 248 ++ .../main/java/com/android/grape/util/Util.kt | 3196 +++++++++++++++++ .../grape/work/CheckAccessibilityWorker.kt | 69 + .../drawable-v24/ic_launcher_foreground.xml | 30 + .../res/drawable/ic_launcher_background.xml | 170 + app/src/main/res/layout/activity_main.xml | 27 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/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/values-night/themes.xml | 7 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/strings.xml | 101 + app/src/main/res/values/themes.xml | 9 + .../res/xml/accessibility_service_config.xml | 7 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../java/com/android/grape/ExampleUnitTest.kt | 17 + build.gradle.kts | 5 + gradle.properties | 23 + gradle/libs.versions.toml | 28 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 + gradlew.bat | 89 + settings.gradle.kts | 24 + 131 files changed, 13846 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/dbnavigator.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/android/grape/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/aidl/android/app/IMikRom.aidl create mode 100644 app/src/main/java/com/android/grape/MainActivity.kt create mode 100644 app/src/main/java/com/android/grape/MainApplication.kt create mode 100644 app/src/main/java/com/android/grape/MainViewModel.kt create mode 100644 app/src/main/java/com/android/grape/data/AfInfo.kt create mode 100644 app/src/main/java/com/android/grape/data/BigoInfo.kt create mode 100644 app/src/main/java/com/android/grape/data/DeviceInfo.kt create mode 100644 app/src/main/java/com/android/grape/job/AutoJobService.kt create mode 100644 app/src/main/java/com/android/grape/job/CheckIpJobService.kt create mode 100644 app/src/main/java/com/android/grape/job/DownloadAppJobService.kt create mode 100644 app/src/main/java/com/android/grape/job/InstallService.kt create mode 100644 app/src/main/java/com/android/grape/job/MonitorService.kt create mode 100644 app/src/main/java/com/android/grape/job/OpenAppService.kt create mode 100644 app/src/main/java/com/android/grape/job/RecoverJobService.kt create mode 100644 app/src/main/java/com/android/grape/job/SendCallbackJobService.kt create mode 100644 app/src/main/java/com/android/grape/job/StartVpnPortJobService.kt create mode 100644 app/src/main/java/com/android/grape/job/StartVpnServerJobService.kt create mode 100644 app/src/main/java/com/android/grape/job/UnInstallService.kt create mode 100644 app/src/main/java/com/android/grape/net/HttpUtils.kt create mode 100644 app/src/main/java/com/android/grape/net/MyGet.kt create mode 100644 app/src/main/java/com/android/grape/net/MyPost.kt create mode 100644 app/src/main/java/com/android/grape/net/NetworkHelper.kt create mode 100644 app/src/main/java/com/android/grape/pseudo/Base64.kt create mode 100644 app/src/main/java/com/android/grape/pseudo/Constants.kt create mode 100644 app/src/main/java/com/android/grape/pseudo/ManifestBuilder.kt create mode 100644 app/src/main/java/com/android/grape/pseudo/PseudoApkSigner.kt create mode 100644 app/src/main/java/com/android/grape/pseudo/SignatureFileGenerator.kt create mode 100644 app/src/main/java/com/android/grape/pseudo/Utils.kt create mode 100644 app/src/main/java/com/android/grape/pseudo/ZipAlignZipOutputStream.kt create mode 100644 app/src/main/java/com/android/grape/receiver/ScriptReceiver.kt create mode 100644 app/src/main/java/com/android/grape/sai/ApkSourceBuilder.kt create mode 100644 app/src/main/java/com/android/grape/sai/BaseSaiPackageInstaller.kt create mode 100644 app/src/main/java/com/android/grape/sai/FlexSaiPackageInstaller.kt create mode 100644 app/src/main/java/com/android/grape/sai/IOUtils.kt create mode 100644 app/src/main/java/com/android/grape/sai/MyBroadcastReceiver.kt create mode 100644 app/src/main/java/com/android/grape/sai/RootedSaiPackageInstaller.kt create mode 100644 app/src/main/java/com/android/grape/sai/RootlessSaiPiBroadcastReceiver.kt create mode 100644 app/src/main/java/com/android/grape/sai/SafUtils.kt create mode 100644 app/src/main/java/com/android/grape/sai/ShellSaiPackageInstaller.kt create mode 100644 app/src/main/java/com/android/grape/sai/ShizukuSaiPackageInstaller.kt create mode 100644 app/src/main/java/com/android/grape/sai/Stopwatch.kt create mode 100644 app/src/main/java/com/android/grape/sai/Utils.kt create mode 100644 app/src/main/java/com/android/grape/sai/apksource/CopyToFileApkSource.kt create mode 100644 app/src/main/java/com/android/grape/sai/apksource/FilterApkSource.kt create mode 100644 app/src/main/java/com/android/grape/sai/apksource/SignerApkSource.kt create mode 100644 app/src/main/java/com/android/grape/sai/filedescriptor/ContentUriFileDescriptor.kt create mode 100644 app/src/main/java/com/android/grape/sai/filedescriptor/DefaultApkSource.kt create mode 100644 app/src/main/java/com/android/grape/sai/filedescriptor/FileDescriptor.kt create mode 100644 app/src/main/java/com/android/grape/sai/filedescriptor/FileUtils.kt create mode 100644 app/src/main/java/com/android/grape/sai/filedescriptor/NormalFileDescriptor.kt create mode 100644 app/src/main/java/com/android/grape/sai/filedescriptor/ZipApkSource.kt create mode 100644 app/src/main/java/com/android/grape/sai/filedescriptor/ZipBackedApkSource.kt create mode 100644 app/src/main/java/com/android/grape/sai/filedescriptor/ZipFileApkSource.kt create mode 100644 app/src/main/java/com/android/grape/sai/inter/ApkSource.kt create mode 100644 app/src/main/java/com/android/grape/sai/inter/SaiPackageInstaller.kt create mode 100644 app/src/main/java/com/android/grape/sai/inter/SaiPiSessionObserver.kt create mode 100644 app/src/main/java/com/android/grape/sai/param/PackageMeta.kt create mode 100644 app/src/main/java/com/android/grape/sai/param/SaiPiSessionParams.kt create mode 100644 app/src/main/java/com/android/grape/sai/param/SaiPiSessionState.kt create mode 100644 app/src/main/java/com/android/grape/sai/param/SaiPiSessionStatus.kt create mode 100644 app/src/main/java/com/android/grape/sai/prefers/DbgPreferencesHelper.kt create mode 100644 app/src/main/java/com/android/grape/sai/prefers/PreferencesHelper.kt create mode 100644 app/src/main/java/com/android/grape/sai/prefers/PreferencesKeys.kt create mode 100644 app/src/main/java/com/android/grape/sai/prefers/PreferencesValues.kt create mode 100644 app/src/main/java/com/android/grape/sai/rootless/AndroidPackageInstallerError.kt create mode 100644 app/src/main/java/com/android/grape/sai/rootless/ConfirmationIntentWrapperActivity2.kt create mode 100644 app/src/main/java/com/android/grape/sai/rootless/RootlessSaiPackageInstaller.kt create mode 100644 app/src/main/java/com/android/grape/sai/shell/MiuiUtils.kt create mode 100644 app/src/main/java/com/android/grape/sai/shell/Shell.kt create mode 100644 app/src/main/java/com/android/grape/sai/shell/ShizukuShell.kt create mode 100644 app/src/main/java/com/android/grape/sai/shell/SuShell.kt create mode 100644 app/src/main/java/com/android/grape/service/MyAccessibilityService.kt create mode 100644 app/src/main/java/com/android/grape/util/AdvertisingIdClient.kt create mode 100644 app/src/main/java/com/android/grape/util/ChangeDeviceInfoUtil.kt create mode 100644 app/src/main/java/com/android/grape/util/ClashUtil.kt create mode 100644 app/src/main/java/com/android/grape/util/DeviceConvertUtil.kt create mode 100644 app/src/main/java/com/android/grape/util/FileUtils.kt create mode 100644 app/src/main/java/com/android/grape/util/MockTools.kt create mode 100644 app/src/main/java/com/android/grape/util/ReflectionHelper.kt create mode 100644 app/src/main/java/com/android/grape/util/ScriptUtil.kt create mode 100644 app/src/main/java/com/android/grape/util/ServiceUtils.kt create mode 100644 app/src/main/java/com/android/grape/util/ShellUtils.java create mode 100644 app/src/main/java/com/android/grape/util/StoragePermissionHelper.kt create mode 100644 app/src/main/java/com/android/grape/util/Util.kt create mode 100644 app/src/main/java/com/android/grape/work/CheckAccessibilityWorker.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/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/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.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/accessibility_service_config.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/test/java/com/android/grape/ExampleUnitTest.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts 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/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000..64f9316 --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,429 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..97f0a8e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file 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.kts b/app/build.gradle.kts new file mode 100644 index 0000000..da50d8d --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.android.grape" + compileSdk = 35 + + defaultConfig { + applicationId = "com.android.grape" + minSdk = 23 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + aidl = true + viewBinding = true + buildConfig = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.work.runtime.ktx) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + implementation ("com.blankj:utilcodex:1.31.1") + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0")) + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.okhttp3:logging-interceptor") + implementation ("dev.rikka.shizuku:api:11.0.3") + implementation ("dev.rikka.shizuku:provider:11.0.3") + implementation ("com.google.code.gson:gson:2.10.1") +} \ 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/android/grape/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/android/grape/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..128757b --- /dev/null +++ b/app/src/androidTest/java/com/android/grape/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.android.grape + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.android.grape", appContext.packageName) + } +} \ 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..6481955 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/aidl/android/app/IMikRom.aidl b/app/src/main/aidl/android/app/IMikRom.aidl new file mode 100644 index 0000000..0153c8c --- /dev/null +++ b/app/src/main/aidl/android/app/IMikRom.aidl @@ -0,0 +1,12 @@ +// IMikRom.aidl +package android.app; + +// Declare any non-default types here with import statements + +interface IMikRom { + String shellExec(String cmd); + String readFile(String path); + void writeFile(String path,String data); + void setEnableApp(String pkgName,boolean isEnable); + boolean isEnableApp(String pkgName); +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/MainActivity.kt b/app/src/main/java/com/android/grape/MainActivity.kt new file mode 100644 index 0000000..ad4fc42 --- /dev/null +++ b/app/src/main/java/com/android/grape/MainActivity.kt @@ -0,0 +1,133 @@ +package com.android.grape + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.android.grape.databinding.ActivityMainBinding +import com.android.grape.job.MonitorService +import com.android.grape.receiver.ScriptReceiver +import com.android.grape.util.ClashUtil +import com.android.grape.util.StoragePermissionHelper +import com.blankj.utilcode.util.LogUtils + +class MainActivity : AppCompatActivity() { + private val viewModel by viewModels() + private lateinit var viewBinding: ActivityMainBinding + private var intentFilter: IntentFilter? = null + private var scriptReceiver: ScriptReceiver? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + viewBinding = ActivityMainBinding.inflate(layoutInflater) + setContentView(viewBinding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + checkPermission() + registerReceiver() + viewBinding.start.setOnClickListener { + try { + ClashUtil.startProxy(this) // 在主线程中调用 + ClashUtil.switchProxyGroup("GLOBAL", "us", "http://127.0.0.1:6170") + } catch (e: Exception) { + LogUtils.log( + Log.ERROR, + "MainActivity", + "startProxyVpn: Failed to start VPN", + e + ) + Toast.makeText( + this, + "Failed to start VPN: " + (if (e.message != null) e.message else "Unknown error"), + Toast.LENGTH_SHORT + ).show() + } + } + viewBinding.stop.setOnClickListener { + ClashUtil.stopProxy(this) +// AutoJsUtil.stopAutojsScript(this) + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + private fun registerReceiver() { + intentFilter = IntentFilter().apply { + addAction("com.ak.lu.SCRIPT_START") + scriptReceiver = ScriptReceiver() + registerReceiver(scriptReceiver, this) + } + } + + override fun onDestroy() { + super.onDestroy() + ClashUtil.unregisterReceiver(this) + unregisterReceiver(scriptReceiver) + } + + private fun checkPermission() { + viewModel.checkAccessibilityService() + StoragePermissionHelper.requestFullStoragePermission( + activity = this, + onGranted = { performFileOperation() }, + onDenied = { showPermissionDeniedDialog() } + ) + } + + private fun performFileOperation() { + // 执行文件读写操作 +// Toast.makeText(this, "文件访问权限已授予", Toast.LENGTH_SHORT).show() + MonitorService.onEvent(this) + } + + private fun showPermissionDeniedDialog() { + AlertDialog.Builder(this) + .setTitle("权限被拒绝") + .setMessage("您拒绝了文件访问权限") + .setPositiveButton("设置") { _, _ -> + StoragePermissionHelper.openAppSettings(this) + } + .setNegativeButton("稍后", null) + .show() + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + StoragePermissionHelper.handlePermissionResult( + activity = this, + requestCode = requestCode, + grantResults = grantResults, + onGranted = { performFileOperation() }, + onDenied = { showPermissionDeniedDialog() } + ) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + StoragePermissionHelper.handleActivityResult( + activity = this, + requestCode = requestCode, + onGranted = { performFileOperation() }, + onDenied = { showPermissionDeniedDialog() } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/MainApplication.kt b/app/src/main/java/com/android/grape/MainApplication.kt new file mode 100644 index 0000000..c14f3e5 --- /dev/null +++ b/app/src/main/java/com/android/grape/MainApplication.kt @@ -0,0 +1,20 @@ +package com.android.grape + +import android.app.Application +import com.blankj.utilcode.util.LogUtils + +class MainApplication: Application() { + companion object { + lateinit var instance: MainApplication + } + override fun onCreate() { + super.onCreate() + instance = this + LogUtils.getConfig().setLog2FileSwitch(true) + initEvn() + } + + fun initEvn(){ + System.setProperty("java.library.path", this.applicationInfo.nativeLibraryDir) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/MainViewModel.kt b/app/src/main/java/com/android/grape/MainViewModel.kt new file mode 100644 index 0000000..1d6345e --- /dev/null +++ b/app/src/main/java/com/android/grape/MainViewModel.kt @@ -0,0 +1,26 @@ +package com.android.grape + +import androidx.lifecycle.ViewModel +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import com.android.grape.service.MyAccessibilityService +import com.android.grape.work.CheckAccessibilityWorker +import java.util.concurrent.TimeUnit + +class MainViewModel:ViewModel() { + + fun checkAccessibilityService() { + val accessibilityServiceEnabled = CheckAccessibilityWorker.isAccessibilityServiceEnabled( + MainApplication.instance, + MyAccessibilityService::class.java + ) + if (!accessibilityServiceEnabled){ + val workRequest = PeriodicWorkRequest.Builder( + CheckAccessibilityWorker::class.java, 15, TimeUnit.MINUTES + ) + .build() + WorkManager.getInstance(MainApplication.instance).enqueue(workRequest) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/data/AfInfo.kt b/app/src/main/java/com/android/grape/data/AfInfo.kt new file mode 100644 index 0000000..c041610 --- /dev/null +++ b/app/src/main/java/com/android/grape/data/AfInfo.kt @@ -0,0 +1,19 @@ +package com.android.grape.data + +class AfInfo { + var advertiserId: String? = null + var model: String? = null + var brand: String? = null + var androidId: String? = null + var xPixels: Int = 0 + var yPixels: Int = 0 + var densityDpi: Int = 0 + var country: String? = null + var batteryLevel: String? = null + var stackInfo: String? = null + var product: String? = null + var network: String? = null + var langCode: String? = null + var cpuAbi: String? = null + var yDp: Long = 0 +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/data/BigoInfo.kt b/app/src/main/java/com/android/grape/data/BigoInfo.kt new file mode 100644 index 0000000..ce85693 --- /dev/null +++ b/app/src/main/java/com/android/grape/data/BigoInfo.kt @@ -0,0 +1,21 @@ +package com.android.grape.data + +class BigoInfo { + var cpuClockSpeed: String? = null + var gaid: String? = null + var userAgent: String? = null + var osLang: String? = null + var osVer: String? = null + var tz: String? = null + var systemCountry: String? = null + var simCountry: String? = null + var romFreeIn: Long = 0 + var resolution: String? = null + var vendor: String? = null + var batteryScale: Int = 0 + var net: String? = null + var dpi: Long = 0 + var romFreeExt: Long = 0 + var dpiF: String? = null + var cpuCoreNum: Long = 0 +} diff --git a/app/src/main/java/com/android/grape/data/DeviceInfo.kt b/app/src/main/java/com/android/grape/data/DeviceInfo.kt new file mode 100644 index 0000000..1b387b1 --- /dev/null +++ b/app/src/main/java/com/android/grape/data/DeviceInfo.kt @@ -0,0 +1,25 @@ +package com.android.grape.data + +class DeviceInfo { + var lang: String? = null + var roProductBrand: String? = null + var roProductModel: String? = null + var roProductManufacturer: String? = null + var roProductDevice: String? = null + var roProductName: String? = null + var roBuildVersionIncremental: String? = null + var roBuildFingerprint: String? = null + var roOdmBuildFingerprint: String? = null + var roProductBuildFingerprint: String? = null + var roSystemBuildFingerprint: String? = null + var roSystemExtBuildFingerprint: String? = null + var roVendorBuildFingerprint: String? = null + var roBuildPlatform: String? = null + var persistSysCloudDrmId: String? = null + var persistSysCloudBatteryCapacity: Int = 0 + var persistSysCloudGpuGlVendor: String? = null + var persistSysCloudGpuGlRenderer: String? = null + var persistSysCloudGpuGlVersion: String? = null + var persistSysCloudGpuEglVendor: String? = null + var persistSysCloudGpuEglVersion: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/job/AutoJobService.kt b/app/src/main/java/com/android/grape/job/AutoJobService.kt new file mode 100644 index 0000000..4c16914 --- /dev/null +++ b/app/src/main/java/com/android/grape/job/AutoJobService.kt @@ -0,0 +1,44 @@ +package com.android.grape.job + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import androidx.core.app.JobIntentService +import com.android.grape.util.ScriptUtil + +class AutoJobService : JobIntentService() { + override fun onHandleWork(intent: Intent) { + val openZzzj = true + + if (openZzzj) { + Handler(Looper.getMainLooper()).postDelayed({ + ScriptUtil.execScript( + this@AutoJobService, + "/sdcard/autojs/main.js" + ) + }, 2000L) + } else { + Handler(Looper.getMainLooper()).postDelayed({ + onEvent( + this@AutoJobService + ) + }, 5000L) + } + } + + companion object { + private const val TAG = "AutoJobService" + private const val jobId = 731 + + fun onEvent(context: Context) { + enqueueWork( + context, + AutoJobService::class.java, jobId, Intent( + context, + AutoJobService::class.java + ) + ) + } + } +} diff --git a/app/src/main/java/com/android/grape/job/CheckIpJobService.kt b/app/src/main/java/com/android/grape/job/CheckIpJobService.kt new file mode 100644 index 0000000..0d64d33 --- /dev/null +++ b/app/src/main/java/com/android/grape/job/CheckIpJobService.kt @@ -0,0 +1,70 @@ +package com.android.grape.job + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.JobIntentService +import com.android.grape.net.MyGet +import com.android.grape.util.Util + +class CheckIpJobService : JobIntentService() { + override fun onHandleWork(intent: Intent) { + if (checkIp()) { + InstallService.onEvent(this) + } else { + Util.isClickRet = false + Util.setInstallRet(false) + Util.clickErrReason = "networkErr" + Util.setFinish(this) + } + } + + private fun checkIp(): Boolean { + try { + var nRetryCount = 0 + do { + val url = "http://8.218.80.200/myipp" + + Log.i(TAG, "start to getIp : " + url + " ; " + Util.ua) + + val resp: String? = MyGet.getData(url, Util.ua) + + Log.i(TAG, "checkIp : $resp") + + if (!resp.isNullOrEmpty() && resp.length >= 15) { + try { + val conn = + resp.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (conn.size > 1 && Util.proxyCountry.equals(conn[1])) { + val ip = conn[0] + Util.delegateIp = ip + return true + } + } catch (e: Exception) { + } + } + Thread.sleep(2000) + } while (nRetryCount++ < 3) + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + + companion object { + private const val TAG = "IOSTQ:CheckIpJobService" + private const val tryNum = 0 + private const val maxTryNum = 5 + private const val jobId = 509 + + fun onEvent(context: Context) { + enqueueWork( + context, + CheckIpJobService::class.java, jobId, Intent( + context, + CheckIpJobService::class.java + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/job/DownloadAppJobService.kt b/app/src/main/java/com/android/grape/job/DownloadAppJobService.kt new file mode 100644 index 0000000..2dd5b4b --- /dev/null +++ b/app/src/main/java/com/android/grape/job/DownloadAppJobService.kt @@ -0,0 +1,51 @@ +package com.android.grape.job + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.JobIntentService +import com.android.grape.util.Util +import com.blankj.utilcode.util.LogUtils + +class DownloadAppJobService : JobIntentService() { + override fun onHandleWork(intent: Intent) { + LogUtils.i(TAG, "start to handle work") + + var succ: Boolean = false + + try { + succ = Util.execDownload(this) + } catch (e: Exception) { + e.printStackTrace() + } + + LogUtils.i(TAG, "download app : $succ") + + if (succ) { + errTime = 0L + StartVpnServerJobService.onEvent(this) +// StartVpnPortJobService.onEvent(this) + } else { + Util.isClickRet = false + Util.setInstallRet(false) + Util.clickErrReason = "downloadErr" + Util.setFinish(this) + } + } + + companion object { + private const val TAG = "IOSTQ:DownloadJobService" + private var errTime = 0L + private const val jobId = 103 + + fun onEvent(context: Context) { + enqueueWork( + context, + DownloadAppJobService::class.java, jobId, Intent( + context, + DownloadAppJobService::class.java + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/job/InstallService.kt b/app/src/main/java/com/android/grape/job/InstallService.kt new file mode 100644 index 0000000..3c211bc --- /dev/null +++ b/app/src/main/java/com/android/grape/job/InstallService.kt @@ -0,0 +1,67 @@ +package com.android.grape.job + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.core.app.JobIntentService +import com.android.grape.util.Util + +class InstallService : JobIntentService() { + override fun onHandleWork(intent: Intent) { + Log.i(TAG, "onHandleWork ...") + + if (Util.installRecord(this)) { + Log.i(TAG, "installRecord succ") + tryNum = 0 + Util.setInstallRet(true) + + println("IOSTQ:isNeedRestored == " + Util.isNeedRestored) + if (Util.isNeedRestored) { + Handler(Looper.getMainLooper()).postDelayed({ + RecoverJobService.onEvent( + this@InstallService + ) + }, 0) + } else { + Handler(Looper.getMainLooper()).postDelayed( + { OpenAppService.onEvent(this@InstallService) }, + 1000L + ) + } + } else { + Log.d(TAG, "onHandleWork: installRecord fail") + tryNum += 1 + if (tryNum <= 5) { + Handler(Looper.getMainLooper()).postDelayed({ + onEvent( + this@InstallService + ) + }, 5000) + } else { //超过5次安装失败直接取消后续 + Log.i(TAG, "install error : " + Util.recordPackageName + " ; " + tryNum) + + Util.setFinish(this) + } + } + } + + + companion object { + private const val TAG = "IOSTQ:InstallService" + private var tryNum = 0 + private const val jobId = 107 + + + fun onEvent(context: Context) { + enqueueWork( + context, + InstallService::class.java, jobId, Intent( + context, + InstallService::class.java + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/job/MonitorService.kt b/app/src/main/java/com/android/grape/job/MonitorService.kt new file mode 100644 index 0000000..fe73cb5 --- /dev/null +++ b/app/src/main/java/com/android/grape/job/MonitorService.kt @@ -0,0 +1,66 @@ +package com.android.grape.job + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.JobIntentService +import com.android.grape.util.ClashUtil +import com.android.grape.util.MockTools +import com.android.grape.util.Util + +/** + * 监听任务 + */ +class MonitorService : JobIntentService() { + override fun onHandleWork(intent: Intent) { + Log.i(TAG, "onHandleWork ... $running") + if (!running) { + running = true + exec() + } + } + + protected fun exec() { + try { + val apkRoot = "chmod 777 $packageCodePath" + MockTools.exec(apkRoot) + Log.i(TAG, "set to top") + Util.setTopApp(this) + + + Log.i(TAG, "auto stop vpn") + ClashUtil.stopProxy(this) + Thread.sleep(3000) + + if (Util.clickTime > 0L) { + Util.clickTime = 0L + SendCallbackJobService.onEvent(this) + } else { + Util.execTask(applicationContext) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + companion object { + private const val TAG = "IOSTQ:MonitorService" + private const val jobId = 101 + + private var running = false + + fun onEvent(context: Context) { + enqueueWork( + context, + MonitorService::class.java, jobId, Intent( + context, + MonitorService::class.java + ) + ) + } + + fun setRunning(runningV: Boolean) { + running = runningV + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/job/OpenAppService.kt b/app/src/main/java/com/android/grape/job/OpenAppService.kt new file mode 100644 index 0000000..04ddaa3 --- /dev/null +++ b/app/src/main/java/com/android/grape/job/OpenAppService.kt @@ -0,0 +1,61 @@ +package com.android.grape.job + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.JobIntentService +import com.android.grape.util.MockTools +import com.android.grape.util.ServiceUtils +import com.android.grape.util.Util +import java.io.IOException + + +class OpenAppService : JobIntentService() { + override fun onHandleWork(intent: Intent) { + println("IOSTQ:isCanAuto() == " + Util.isCanAuto) + println("IOSTQ:getCanAutoLc() == " + Util.canAutoLc) + Util.recordPackageName?.let { + ServiceUtils.setEnableApp(it, true) + } + ServiceUtils.setEnableApp("org.mozilla.firefox", true) + ServiceUtils.setEnableApp("com.google.android.webview", true) + ServiceUtils.setEnableApp("com.android.chrome", true) + ServiceUtils.setEnableApp("com.UCMobile", true) + if (Util.isCanAuto && Util.canAutoLc.isNotEmpty()) { + MockTools.exec("pm grant org.autojs.autojs android.permission.READ_EXTERNAL_STORAGE") //sdcard权限 + MockTools.exec("pm grant org.autojs.autojs android.permission.SYSTEM_ALERT_WINDOW") //悬浮窗权限 + MockTools.exec("settings put secure enabled_accessibility_services org.autojs.autojs/com.stardust.autojs.core.accessibility.AccessibilityService") + Util.doScript(this, Util.AUTO_JSPACKAGENAME) //autojs + } + try { + if (Util.isNeedRestored) { + Log.d("IOSTQ:", "执行留存任务") + Util.setRrInfo(this) + } else { + Log.d("IOSTQ:", "执行新装任务") + Util.setInfo(this) + } + } catch (e: IOException) { + e.printStackTrace() + } + + // Util.hookOpenApp(this); + Util.openRecordApp(this) + } + + companion object { + private const val TAG = "IOSTQ:OpenAppService" + + private const val jobId = 111 + + fun onEvent(context: Context) { + enqueueWork( + context, + OpenAppService::class.java, jobId, Intent( + context, + OpenAppService::class.java + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/job/RecoverJobService.kt b/app/src/main/java/com/android/grape/job/RecoverJobService.kt new file mode 100644 index 0000000..3271525 --- /dev/null +++ b/app/src/main/java/com/android/grape/job/RecoverJobService.kt @@ -0,0 +1,52 @@ +package com.android.grape.job + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.core.app.JobIntentService +import com.android.grape.util.Util + +class RecoverJobService : JobIntentService() { + override fun onHandleWork(intent: Intent) { + Log.i(TAG, "onHandleWork") + + var succ = false + + try { + succ = Util.recoverRecordData(this) + } catch (e: Exception) { + e.printStackTrace() + } + + if (succ) { + Handler(Looper.getMainLooper()).postDelayed( + { OpenAppService.onEvent(this@RecoverJobService) }, + 3000L + ) + } else { + Handler(Looper.getMainLooper()).postDelayed({ + onEvent( + this@RecoverJobService + ) + }, 3000L) + } + } + + companion object { + private const val TAG = "IOSTQ:RecoverJobService" + + private const val jobId = 109 + + fun onEvent(context: Context) { + enqueueWork( + context, + RecoverJobService::class.java, jobId, Intent( + context, + RecoverJobService::class.java + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/job/SendCallbackJobService.kt b/app/src/main/java/com/android/grape/job/SendCallbackJobService.kt new file mode 100644 index 0000000..ce1b4b5 --- /dev/null +++ b/app/src/main/java/com/android/grape/job/SendCallbackJobService.kt @@ -0,0 +1,293 @@ +package com.android.grape.job + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import androidx.core.app.JobIntentService +import com.android.grape.net.MyPost +import com.android.grape.util.Util +import com.blankj.utilcode.util.LogUtils +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import org.json.JSONObject +import java.io.File +import java.util.concurrent.TimeUnit + +class SendCallbackJobService : JobIntentService() { + override fun onHandleWork(intent: Intent) { + LogUtils.i(TAG, "onHandleWork") + + if (Util.isInstallRet()) { + SendBackup() + + if (Util.isNeedRestored) { + sendReloginEvent() + } else { + sendLogEvent() + } + } + Util.setFinish(this@SendCallbackJobService) + } + + + fun sendReloginEvent(): Boolean { + try { + val url = "http://39.103.73.250/tt/ddj/preCallback!rr.do" + + val paramsJo = JSONObject() + val datajson = JSONObject() + + if (Util.isNeedBackup) { + val cachejson = JSONObject() + cachejson.put("firstInstallTime", Util.installTime) + cachejson.put("lastUpdateTime", Util.lastUpdateTime) + cachejson.put("installServerTimeFromGP", Util.installServerTimeFromGP) + cachejson.put("clickServerTimeToGP", Util.clickServerTimeFromGP) + cachejson.put("installTimeFromGP", Util.instalTimeFromGp) + datajson.put("cacheJson", cachejson) + } + + paramsJo.put("recordId", Util.recordId) + paramsJo.put("preClickRecordId", Util.preClickRecordId) + paramsJo.put("dataJson", datajson) + paramsJo.put("reloginRecordId", Util.reloginRecordId) + val clickInfoJo = JSONObject() + clickInfoJo.put("clickRet", Util.isClickRet) + clickInfoJo.put("clickIp", Util.delegateIp) + clickInfoJo.put("clickTime", Util.clickTime) + clickInfoJo.put("clickErrReason", Util.clickErrReason) + paramsJo.put("clickInfo", clickInfoJo) + + val installInfoJo = JSONObject() + installInfoJo.put("installRet", Util.isInstallRet()) + installInfoJo.put("installTime", Util.installTime) + paramsJo.put("installInfo", installInfoJo) + + + if (null != Util.backupResult) { + paramsJo.put("backUpFiles", Util.backupResult) + } + + val logInfoJo = JSONObject() + logInfoJo.put("afLog", Util.getAfLog() + "\r\n" + Util.logBuffer.toString()) + // logInfoJo.put("setConfLog", Util.getParamsJson()); + paramsJo.put("logInfo", logInfoJo) + var params: String? = null + params = paramsJo?.toString() ?: "error" + + var nRetryCount = 0 + do { + val ret: String? = MyPost.postData(params.toByteArray(charset("utf-8")), url) + LogUtils.i(TAG, "ret:$ret") + val jo = ret?.let { JSONObject(it) } + if (jo?.getInt("code") == 1) { + return true + } + } while (nRetryCount++ < 3) + return false + } catch (e: Exception) { + e.printStackTrace() + } + + return false + } + + fun sendLogEvent(): Boolean { + try { + val url = "http://39.103.73.250/tt/ddj/preCallback!install.do" + + val paramsJo = JSONObject() + val datajson = JSONObject() + + if (Util.isNeedBackup) { + val cachejson = JSONObject() + cachejson.put("firstInstallTime", Util.installTime) + cachejson.put("lastUpdateTime", Util.lastUpdateTime) + cachejson.put("installServerTimeFromGP", Util.installServerTimeFromGP) + cachejson.put("clickServerTimeToGP", Util.clickServerTimeFromGP) + cachejson.put("installTimeFromGP", Util.instalTimeFromGp) + datajson.put("cacheJson", cachejson) + } + + paramsJo.put("recordId", Util.recordId) + paramsJo.put("preClickRecordId", Util.preClickRecordId) + paramsJo.put("dataJson", datajson) + val clickInfoJo = JSONObject() + clickInfoJo.put("clickRet", Util.isClickRet) + clickInfoJo.put("clickIp", Util.delegateIp) + clickInfoJo.put("clickTime", Util.clickTime) + clickInfoJo.put("clickErrReason", Util.clickErrReason) + paramsJo.put("clickInfo", clickInfoJo) + + val installInfoJo = JSONObject() + installInfoJo.put("installRet", Util.isInstallRet()) + installInfoJo.put("installTime", Util.installTime) + paramsJo.put("installInfo", installInfoJo) + + val logInfoJo = JSONObject() + logInfoJo.put("afLog", Util.getAfLog() + "\r\n" + Util.logBuffer.toString()) + // logInfoJo.put("setConfLog", Util.getParamsJson()); + logInfoJo.put("hookVer", Util.HkVer()) + logInfoJo.put("afVer", Util.getAppAfVer()) + logInfoJo.put("afApp", Util.afApp) + paramsJo.put("logInfo", logInfoJo) + + if (null != Util.backupResult) { + paramsJo.put("backUpFiles", Util.backupResult) + } + var params: String? = null + params = paramsJo?.toString() ?: "error" + + var nRetryCount = 0 + do { + val ret: String = MyPost.postData( params.toByteArray(charset("utf-8")), url)?:"" + LogUtils.i(TAG, "ret:$ret") + val jo = JSONObject(ret) + if (jo.getInt("code") == 1) { + return true + } + } while (nRetryCount++ < 3) + + return false + } catch (e: Exception) { + e.printStackTrace() + } + + return false + } + + + protected fun SendBackup() { + LogUtils.i(TAG, "onHandleWork") + var succ = false + try { + val fileName: String? = Util.backFileName + val fileName1: String = Util.backFileName1?:"" + val fileName2: String = Util.backFileName2?:"" + + LogUtils.i( + TAG, + "backupDataFile to $fileName;$fileName1;$fileName2" + ) + if (fileName != null) { + for (i in 0..4) { + if (sendBackupEvent(fileName, fileName1)) { + succ = true + break + } + } + if (succ) { + Util.delFileSh(fileName) + Util.delFileSh(fileName1) + Util.delFileSh(fileName2) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun sendBackupEvent(fileName: String, fileName1: String?): Boolean { + val file = File(fileName) + var result = false + var file1: File? = null + if (fileName1 != null && fileName1.length > 0) { + file1 = File(fileName1) + } + + Util.chownSh(this, fileName, Util.getMainUserAndGroup(this)) + var url = "http://192.168.1.111/tt/ddj/backup.do" + if (Util.backUpServerIp != "") { + url = "http://" + Util.backUpServerIp + "/tt/ddj/backup.do" + } + LogUtils.i(TAG, "sendBackupEvent-> file length = " + file.length()) + + try { + val mOkHttpClient = OkHttpClient().newBuilder() + .connectTimeout(60, TimeUnit.SECONDS) //设置超时时间 + .readTimeout(60, TimeUnit.SECONDS) //设置读取超时时间 + .writeTimeout(60, TimeUnit.SECONDS) //设置写入超时时间 + .build() + //初始化Handler + val okHttpHandler = Handler(Looper.getMainLooper()) + + //补全请求地址 + val builder = MultipartBody.Builder() + //设置类型 + builder.setType(MultipartBody.FORM) + + //追加参数 + builder.addFormDataPart("recordId", "${Util.recordId}") + builder.addFormDataPart("afVer", Util.getAppAfVer()) + + if (file.exists() && file.length() > 0) { + builder.addFormDataPart( + "file1", + file.name, + RequestBody.create("multipart/form-data".toMediaTypeOrNull(), file) + ) + } + + if (file1 != null && file1.exists()) { + builder.addFormDataPart( + "file2", + file1.name, + RequestBody.create("multipart/form-data".toMediaTypeOrNull(), file1) + ) + } + + //创建RequestBody + val body: RequestBody = builder.build() + LogUtils.d(TAG, "sendBackupEvent-> url = $url") + LogUtils.d(TAG, "sendBackupEvent-> recordId= " + Util.recordId) + //创建Request + val request = Request.Builder().url(url).post(body).build() + //单独设置参数 比如读取超时时间 + val call = mOkHttpClient.newBuilder().writeTimeout(50, TimeUnit.SECONDS).build() + .newCall(request) + + val response = call.execute() + + if (response.isSuccessful) { + val ret = response.body!!.string() + val retJo = JSONObject(ret) + if (retJo.getInt("code") == 1) { + if (retJo.has("backUpFiles")) { + Util.backupResult = retJo.getJSONObject("backUpFiles") + } + result = true + } + } + } catch (e: Exception) { + LogUtils.e(TAG, e.toString()) + result = false + } + + + return result + } + + internal class PostRet { + var succ: Int = 0 + } + + companion object { + private const val TAG = "IOSTQ:SendCallbackJobService" + + private const val jobId = 700 + + fun onEvent(context: Context) { + enqueueWork( + context, + SendCallbackJobService::class.java, jobId, Intent( + context, + SendCallbackJobService::class.java + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/job/StartVpnPortJobService.kt b/app/src/main/java/com/android/grape/job/StartVpnPortJobService.kt new file mode 100644 index 0000000..7d32b23 --- /dev/null +++ b/app/src/main/java/com/android/grape/job/StartVpnPortJobService.kt @@ -0,0 +1,45 @@ +package com.android.grape.job + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.core.app.JobIntentService +import com.android.grape.util.ClashUtil +import com.android.grape.util.Util + +class StartVpnPortJobService : JobIntentService() { + override fun onHandleWork(intent: Intent) { + Log.i(TAG, "start to handle work") + if (ClashUtil.checkProxy( this)) { + Handler(Looper.getMainLooper()).postDelayed({ + StartVpnServerJobService.onEvent( + this@StartVpnPortJobService + ) + }, 1000L) + } else { + Util.setInstallRet(false) + Util.isClickRet = false + Util.setFinish(this) + } + } + + companion object { + private const val TAG = "IOSTQ:StartVpnPort" + + private const val jobId = 503 + + + fun onEvent(context: Context) { + enqueueWork( + context, + StartVpnPortJobService::class.java, jobId, Intent( + context, + StartVpnPortJobService::class.java + ) + ) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/job/StartVpnServerJobService.kt b/app/src/main/java/com/android/grape/job/StartVpnServerJobService.kt new file mode 100644 index 0000000..7e3d12e --- /dev/null +++ b/app/src/main/java/com/android/grape/job/StartVpnServerJobService.kt @@ -0,0 +1,60 @@ +package com.android.grape.job + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.core.app.JobIntentService +import com.android.grape.MainApplication +import com.android.grape.util.ClashUtil +import com.android.grape.util.Util + +/** + * 本地vpn功能 + */ +class StartVpnServerJobService : JobIntentService() { + override fun onHandleWork(intent: Intent) { + Log.i(TAG, "start to handle work") + + if (exec()) { + Handler(Looper.getMainLooper()).postDelayed( + { CheckIpJobService.onEvent(this@StartVpnServerJobService) }, + 5000L + ) + } else { + Handler(Looper.getMainLooper()).postDelayed({ + onEvent( + this@StartVpnServerJobService + ) + }, 1000L) + } + } + + private fun exec(): Boolean { + ClashUtil.startProxy(MainApplication.instance) + ClashUtil.switchProxyGroup("GLOBAL", Util.proxyCountry, "http://127.0.0.1:6170") + if (!ClashUtil.checkProxy(MainApplication.instance)) { + println("IOSTQ:start vpn error") + return false + } + println("IOSTQ:start vpn ok") + return true + } + + companion object { + private const val TAG = "IOSTQ:StartVpnServer" + + private const val jobId = 505 + + fun onEvent(context: Context) { + enqueueWork( + context, + StartVpnServerJobService::class.java, jobId, Intent( + context, + StartVpnServerJobService::class.java + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/job/UnInstallService.kt b/app/src/main/java/com/android/grape/job/UnInstallService.kt new file mode 100644 index 0000000..350de54 --- /dev/null +++ b/app/src/main/java/com/android/grape/job/UnInstallService.kt @@ -0,0 +1,36 @@ +package com.android.grape.job + +import android.content.Context +import android.content.Intent +import androidx.core.app.JobIntentService +import com.android.grape.util.MockTools +import com.android.grape.util.Util + + +class UnInstallService : JobIntentService() { + override fun onHandleWork(intent: Intent) { + + Util.backUp(this) + Util.setInstallRet(true) + MockTools.exec("pm clear " + Util.recordPackageName) + Util.delFiles(this@UnInstallService) + + Util.setFinish(this@UnInstallService) + } + + companion object { + private const val TAG = "IOSTQ:UnInstallService" + + private const val jobId = 115 + + fun onEvent(context: Context) { + enqueueWork( + context, + UnInstallService::class.java, jobId, Intent( + context, + UnInstallService::class.java + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/net/HttpUtils.kt b/app/src/main/java/com/android/grape/net/HttpUtils.kt new file mode 100644 index 0000000..7683a36 --- /dev/null +++ b/app/src/main/java/com/android/grape/net/HttpUtils.kt @@ -0,0 +1,267 @@ +package com.android.grape.net + +import android.util.Log +import com.blankj.utilcode.util.LogUtils +import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException +import java.util.concurrent.TimeUnit + +object HttpUtils { + + // OkHttp客户端配置 + private val client: OkHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .addInterceptor(LoggingInterceptor()) // 添加日志拦截器 + .build() + } + + // JSON媒体类型 + private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull() + + // 回调接口 + interface HttpCallback { + fun onSuccess(response: String, code: Int) + fun onFailure(error: String, code: Int?) + } + + // 日志拦截器 + private class LoggingInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val url = request.url.toString() + val method = request.method + + // 记录请求信息 + LogUtils.d("HTTP_REQUEST", "URL: $url") + LogUtils.d("HTTP_REQUEST", "Method: $method") + + if ("POST" == method) { + request.body?.let { body -> + if (body is FormBody) { + val formBody = StringBuilder() + for (i in 0 until body.size) { + formBody.append("${body.name(i)}:${body.value(i)}; ") + } + Log.d("HTTP_REQUEST", "Form Data: $formBody") + } else if (body is MultipartBody) { + Log.d("HTTP_REQUEST", "Multipart Form Data") + } else { + Log.d("HTTP_REQUEST", "Body: ${body.contentType()}") + } + } + } + + val response = chain.proceed(request) + val responseBody = response.peekBody(Long.MAX_VALUE) + val responseString = responseBody.string() + + // 记录响应信息 + LogUtils.d("HTTP_RESPONSE", "Code: ${response.code}") + LogUtils.d("HTTP_RESPONSE", "Response: $responseString") + + return response.newBuilder() + .body(responseBody) + .build() + } + } + + /** + * GET 请求(异步) + * + * @param url 请求URL + * @param params 查询参数(可选) + * @param headers 请求头(可选) + * @param callback 回调接口 + */ + fun getAsync( + url: String, + params: Map? = null, + headers: Map? = null, + callback: HttpCallback + ) { + val request = buildGetRequest(url, params, headers) + executeRequestAsync(request, callback) + } + + /** + * POST 请求(异步) + * + * @param url 请求URL + * @param body 请求体(JSON格式) + * @param headers 请求头(可选) + * @param callback 回调接口 + */ + fun postAsync( + url: String, + body: String, + headers: Map? = null, + callback: HttpCallback + ) { + val requestBody = body.toRequestBody(JSON_MEDIA_TYPE) + val request = buildPostRequest(url, requestBody, headers) + executeRequestAsync(request, callback) + } + + /** + * POST Form Data 请求(异步) + * + * @param url 请求URL + * @param formData 表单数据 + * @param headers 请求头(可选) + * @param callback 回调接口 + */ + fun postFormAsync( + url: String, + formData: Map, + headers: Map? = null, + callback: HttpCallback + ) { + val formBody = buildFormBody(formData) + val request = buildPostRequest(url, formBody, headers) + executeRequestAsync(request, callback) + } + + /** + * GET 请求(同步) + * + * @param url 请求URL + * @param params 查询参数(可选) + * @param headers 请求头(可选) + * @return Pair<响应内容, 状态码> + */ + fun getSync( + url: String, + params: Map? = null, + headers: Map? = null + ): Pair { + val request = buildGetRequest(url, params, headers) + return executeRequestSync(request) + } + + /** + * POST 请求(同步) + * + * @param url 请求URL + * @param body 请求体(JSON格式) + * @param headers 请求头(可选) + * @return Pair<响应内容, 状态码> + */ + fun postSync( + url: String, + body: String, + headers: Map? = null + ): Pair { + val requestBody = body.toRequestBody(JSON_MEDIA_TYPE) + val request = buildPostRequest(url, requestBody, headers) + return executeRequestSync(request) + } + + /** + * 取消所有请求 + */ + fun cancelAllRequests() { + client.dispatcher.cancelAll() + } + + // 内部方法 -------------------------------- + + private fun buildGetRequest( + url: String, + params: Map?, + headers: Map? + ): Request { + val httpUrlBuilder = url.toHttpUrlOrNull()?.newBuilder() + ?: throw IllegalArgumentException("Invalid URL: $url") + + // 添加查询参数 + params?.forEach { (key, value) -> + httpUrlBuilder.addQueryParameter(key, value) + } + + val requestBuilder = Request.Builder().url(httpUrlBuilder.build()) + + // 添加请求头 + addHeaders(requestBuilder, headers) + + return requestBuilder.build() + } + + private fun buildPostRequest( + url: String, + body: RequestBody, + headers: Map? + ): Request { + val requestBuilder = Request.Builder() + .url(url) + .post(body) + + // 添加请求头 + addHeaders(requestBuilder, headers) + + return requestBuilder.build() + } + + private fun addHeaders( + builder: Request.Builder, + headers: Map? + ) { + headers?.forEach { (key, value) -> + builder.addHeader(key, value) + } + } + + private fun buildFormBody(formData: Map): RequestBody { + val formBodyBuilder = FormBody.Builder() + formData.forEach { (key, value) -> + formBodyBuilder.add(key, value) + } + return formBodyBuilder.build() + } + + private fun executeRequestAsync( + request: Request, + callback: HttpCallback + ) { + client.newCall(request).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val responseBody = response.body?.string() + val code = response.code + + if (response.isSuccessful && responseBody != null) { + callback.onSuccess(responseBody, code) + } else { + callback.onFailure( + responseBody ?: "Empty response", + code + ) + } + } + + override fun onFailure(call: Call, e: IOException) { + callback.onFailure(e.message ?: "Unknown network error", null) + } + }) + } + + private fun executeRequestSync(request: Request): Pair { + return try { + val response = client.newCall(request).execute() + val responseBody = response.body?.string() + val code = response.code + + if (response.isSuccessful && responseBody != null) { + Pair(responseBody, code) + } else { + Pair(null, code) + } + } catch (e: IOException) { + Pair(null, -1) // 网络错误状态码 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/net/MyGet.kt b/app/src/main/java/com/android/grape/net/MyGet.kt new file mode 100644 index 0000000..fc91211 --- /dev/null +++ b/app/src/main/java/com/android/grape/net/MyGet.kt @@ -0,0 +1,241 @@ +package com.android.grape.net + +import android.content.Context +import android.util.Log +import java.io.BufferedInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL + + +object MyGet { + private const val TAG = "MyGet" + + fun getData(url_: String, ua: String?): String? { + return getData(url_, ua, 0) + } + + fun getData( url_: String, ua: String?, num: Int): String? { + Log.i(TAG, "start getData : $url_") + + var result: String? = null + var httpUrlConnection: HttpURLConnection? = null + var inStrm: InputStream? = null + var baos: ByteArrayOutputStream? = null + var bis: BufferedInputStream? = null + + try { + val url = URL(url_) + httpUrlConnection = url.openConnection() as HttpURLConnection + + httpUrlConnection.allowUserInteraction = true + httpUrlConnection!!.doOutput = false + httpUrlConnection.doInput = true + httpUrlConnection.useCaches = false + httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428 + httpUrlConnection.setRequestProperty("Content-Type", "text/html;charset=utf-8") + httpUrlConnection.setRequestProperty("User-Agent", ua) + httpUrlConnection.instanceFollowRedirects = false + httpUrlConnection.requestMethod = "GET" + httpUrlConnection.connectTimeout = 60000 + httpUrlConnection.readTimeout = 60000 + + val statusCode = httpUrlConnection.responseCode + Log.i(TAG, "getData status code : $statusCode") + + val location = httpUrlConnection.getHeaderField("Location") + + if (location != null && location.length > 0) { + return if (location.startsWith("http")) { + if (num < 6) { + getData(location, ua, num + 1) + } else { + "jumpOver" + } + } else { + "unknowSchema__$location" + } + } else if (statusCode >= 200 && statusCode < 300) { + inStrm = httpUrlConnection.inputStream + + baos = ByteArrayOutputStream() + + bis = BufferedInputStream(inStrm) + val buf = ByteArray(1024) + var readSize = -1 + + while ((bis.read(buf).also { readSize = it }) != -1) { + baos.write(buf, 0, readSize) + } + + val data = baos.toByteArray() + + return String(data, charset("UTF-8")) + } else { + return "errorOccur" + } + } catch (e: Exception) { + e.printStackTrace() + result = null + } finally { + if (baos != null) { + try { + baos.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + if (bis != null) { + try { + bis.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + + if (inStrm != null) { + try { + inStrm.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + + if (httpUrlConnection != null) { + httpUrlConnection.disconnect() + httpUrlConnection = null + } + + System.gc() + } + + return null + } + + fun getData(context: Context?, url_: String, ua: String?, forwardIp: String?): String? { + return getData(context, url_, ua, forwardIp, 0) + } + + fun getData( + context: Context?, + url_: String, + ua: String?, + forwardIp: String?, + num: Int + ): String? { + var url_ = url_ + url_ = url_.replace(" ", "") + Log.i(TAG, "start getData : $url_") + + var result: String? = null + var httpUrlConnection: HttpURLConnection? = null + var inStrm: InputStream? = null + var baos: ByteArrayOutputStream? = null + var bis: BufferedInputStream? = null + + try { + val url = URL(url_) + httpUrlConnection = url.openConnection() as HttpURLConnection + + httpUrlConnection.allowUserInteraction = true + httpUrlConnection!!.doOutput = false + httpUrlConnection.doInput = true + httpUrlConnection.useCaches = false + httpUrlConnection.instanceFollowRedirects = false + httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428 + httpUrlConnection.setRequestProperty("Content-Type", "text/html;charset=utf-8") + httpUrlConnection.setRequestProperty("User-Agent", ua) + httpUrlConnection.setRequestProperty("X-Forwarded-For", forwardIp) + httpUrlConnection.requestMethod = "GET" + httpUrlConnection.connectTimeout = 60000 + httpUrlConnection.readTimeout = 60000 + + val statusCode = httpUrlConnection.responseCode + Log.i(TAG, "getData status code : $statusCode") + + val location = httpUrlConnection.getHeaderField("Location") + + if (location != null && location.length > 0) { + if (location.startsWith("http")) { + return if (num >= 6) { + "jumpOver" + } else { + getData(context, location, ua, forwardIp, num + 1) + } + } else if (location.contains("://")) { + return "unknowSchema__$location" + } else { + if (num >= 6) { + return "jumpOver" + } else { + val arr = + url_.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + var newloc = "" + if (location.startsWith("/")) { + newloc = arr[0] + "//" + arr[2] + "/" + location + } else { + val pos = url_.lastIndexOf("/") + newloc = url_.substring(0, pos) + "/" + location + } + return getData(context, newloc, ua, forwardIp, num + 1) + } + } + } else if (statusCode >= 200 && statusCode < 300) { + inStrm = httpUrlConnection.inputStream + + baos = ByteArrayOutputStream() + + bis = BufferedInputStream(inStrm) + val buf = ByteArray(1024) + var readSize = -1 + + while ((bis.read(buf).also { readSize = it }) != -1) { + baos.write(buf, 0, readSize) + } + + val data = baos.toByteArray() + + return String(data, charset("UTF-8")) + } else { + return "errorOccur" + } + } catch (e: Exception) { + e.printStackTrace() + result = null + } finally { + if (baos != null) { + try { + baos.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + if (bis != null) { + try { + bis.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + + if (inStrm != null) { + try { + inStrm.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + + if (httpUrlConnection != null) { + httpUrlConnection.disconnect() + httpUrlConnection = null + } + + System.gc() + } + + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/net/MyPost.kt b/app/src/main/java/com/android/grape/net/MyPost.kt new file mode 100644 index 0000000..204e960 --- /dev/null +++ b/app/src/main/java/com/android/grape/net/MyPost.kt @@ -0,0 +1,127 @@ +package com.android.grape.net + +import java.io.BufferedInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.io.UnsupportedEncodingException +import java.net.HttpURLConnection +import java.net.URL + +object MyPost { + fun postData(byt: ByteArray?, url: String): String? { + val bytes: ByteArray? = postDataBytes(byt, url) + + if (bytes != null && bytes.isNotEmpty()) { + try { + return String(bytes, charset("UTF-8")) + } catch (e: UnsupportedEncodingException) { + e.printStackTrace() + } + } + + + return null + } + + fun postDataBytes(byt: ByteArray?, url: String): ByteArray? { + var result: String? = null + var httpUrlConnection: HttpURLConnection? = null + var inStrm: InputStream? = null + var baos: ByteArrayOutputStream? = null + var bis: BufferedInputStream? = null + + try { + httpUrlConnection = getHttpURLConnection(url) + + httpUrlConnection.allowUserInteraction = true + httpUrlConnection.doOutput = true + httpUrlConnection.doInput = true + httpUrlConnection.useCaches = false + httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428 + httpUrlConnection.setRequestProperty( + "Content-type", + "application/x-java-serialized-object" + ) + // httpUrlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + httpUrlConnection.requestMethod = "POST" + httpUrlConnection.connectTimeout = 20000 + var outStrm: OutputStream? = null + + + outStrm = httpUrlConnection.outputStream + + + outStrm.write(byt) + outStrm.flush() + outStrm.close() + + inStrm = httpUrlConnection.inputStream + + baos = ByteArrayOutputStream() + + bis = BufferedInputStream(inStrm) + val buf = ByteArray(1024) + var readSize = -1 + + while ((bis.read(buf).also { readSize = it }) != -1) { + baos.write(buf, 0, readSize) + } + + val data = baos.toByteArray() + + return data + } catch (e: Exception) { + e.printStackTrace() + result = null + } finally { + if (baos != null) { + try { + baos.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + if (bis != null) { + try { + bis.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + + if (inStrm != null) { + try { + inStrm.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + + if (httpUrlConnection != null) { + httpUrlConnection.disconnect() + httpUrlConnection = null + } + + System.gc() + } + + return null + } + + const val dynamicPort0: Int = 20380 + const val dynamicPort: Int = dynamicPort0 + const val dynamicPort1: Int = 30379 + + fun getHttpURLConnection( + _url: String + ): HttpURLConnection { + val url = URL(_url) + + var httpUrlConnection: HttpURLConnection? = null + + httpUrlConnection = url.openConnection() as HttpURLConnection + return httpUrlConnection + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/net/NetworkHelper.kt b/app/src/main/java/com/android/grape/net/NetworkHelper.kt new file mode 100644 index 0000000..d1b2698 --- /dev/null +++ b/app/src/main/java/com/android/grape/net/NetworkHelper.kt @@ -0,0 +1,5 @@ +package com.android.grape.net + +class NetworkHelper { + +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/pseudo/Base64.kt b/app/src/main/java/com/android/grape/pseudo/Base64.kt new file mode 100644 index 0000000..22e5dd2 --- /dev/null +++ b/app/src/main/java/com/android/grape/pseudo/Base64.kt @@ -0,0 +1,819 @@ +package com.android.grape.pseudo + +import java.io.UnsupportedEncodingException + + +/** + * Utilities for encoding and decoding the Base64 representation of + * binary data. See RFCs [2045](http://www.ietf.org/rfc/rfc2045.txt) and [3548](http://www.ietf.org/rfc/rfc3548.txt). + */ +object Base64 { + /** + * Default values for encoder/decoder flags. + */ + const val DEFAULT: Int = 0 + + /** + * Encoder flag bit to omit the padding '=' characters at the end + * of the output (if any). + */ + const val NO_PADDING: Int = 1 + + /** + * Encoder flag bit to omit all line terminators (i.e., the output + * will be on one long line). + */ + const val NO_WRAP: Int = 2 + + /** + * Encoder flag bit to indicate lines should be terminated with a + * CRLF pair instead of just an LF. Has no effect if `NO_WRAP` is specified as well. + */ + const val CRLF: Int = 4 + + /** + * Encoder/decoder flag bit to indicate using the "URL and + * filename safe" variant of Base64 (see RFC 3548 section 4) where + * `-` and `_` are used in place of `+` and + * `/`. + */ + const val URL_SAFE: Int = 8 + + // -------------------------------------------------------- + // decoding + // -------------------------------------------------------- + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + * + * The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param str the input String to decode, which is converted to + * bytes using the default charset + * @param flags controls certain features of the decoded output. + * Pass `DEFAULT` to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + fun decode(str: String, flags: Int): ByteArray { + return decode(str.toByteArray(), flags) + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + * + * The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the input array to decode + * @param flags controls certain features of the decoded output. + * Pass `DEFAULT` to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + fun decode(input: ByteArray, flags: Int): ByteArray { + return decode(input, 0, input.size, flags) + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + * + * The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the data to decode + * @param offset the position within the input array at which to start + * @param len the number of bytes of input to decode + * @param flags controls certain features of the decoded output. + * Pass `DEFAULT` to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + fun decode(input: ByteArray, offset: Int, len: Int, flags: Int): ByteArray { + // Allocate space for the most data the input could represent. + // (It could contain less if it contains whitespace, etc.) + val decoder = Decoder(flags, ByteArray(len * 3 / 4)) + + require(decoder.process(input, offset, len, true)) { "bad base-64" } + + // Maybe we got lucky and allocated exactly enough output space. + if (decoder.op == (decoder.output?.size ?: 0)) { + return decoder.output?: ByteArray(0) + } + + // Need to shorten the array, so allocate a new one of the + // right size and copy. + val temp = ByteArray(decoder.op) + decoder.output?.let { System.arraycopy(it, 0, temp, 0, decoder.op) } + return temp + } + + // -------------------------------------------------------- + // encoding + // -------------------------------------------------------- + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing `DEFAULT` results in output that + * adheres to RFC 2045. + */ + fun encodeToString(input: ByteArray, flags: Int): String { + try { + return String(encode(input, flags), charset("US-ASCII")) + } catch (e: UnsupportedEncodingException) { + // US-ASCII is guaranteed to be available. + throw AssertionError(e) + } + } + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing `DEFAULT` results in output that + * adheres to RFC 2045. + */ + fun encodeToString(input: ByteArray, offset: Int, len: Int, flags: Int): String { + try { + return String(encode(input, offset, len, flags), charset("US-ASCII")) + } catch (e: UnsupportedEncodingException) { + // US-ASCII is guaranteed to be available. + throw AssertionError(e) + } + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing `DEFAULT` results in output that + * adheres to RFC 2045. + */ + fun encode(input: ByteArray, flags: Int): ByteArray { + return encode(input, 0, input.size, flags) + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing `DEFAULT` results in output that + * adheres to RFC 2045. + */ + fun encode(input: ByteArray, offset: Int, len: Int, flags: Int): ByteArray { + val encoder = Encoder(flags, null) + + // Compute the exact length of the array we will produce. + var output_len = len / 3 * 4 + + // Account for the tail of the data and the padding bytes, if any. + if (encoder.do_padding) { + if (len % 3 > 0) { + output_len += 4 + } + } else { + when (len % 3) { + 0 -> {} + 1 -> output_len += 2 + 2 -> output_len += 3 + } + } + + // Account for the newlines, if any. + if (encoder.do_newline && len > 0) { + output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) * + (if (encoder.do_cr) 2 else 1) + } + + encoder.output = ByteArray(output_len) + encoder.process(input, offset, len, true) + + assert(encoder.op == output_len) + + return encoder.output?: ByteArray(0) + } + + // -------------------------------------------------------- + // shared code + // -------------------------------------------------------- + /* package */ + internal abstract class Coder { + var output: ByteArray? = null + var op: Int = 0 + + /** + * Encode/decode another block of input data. this.output is + * provided by the caller, and must be big enough to hold all + * the coded data. On exit, this.opwill be set to the length + * of the coded data. + * + * @param finish true if this is the final call to process for + * this object. Will finalize the coder state and + * include any final bytes in the output. + * @return true if the input so far is good; false if some + * error has been detected in the input stream.. + */ + abstract fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean + + /** + * @return the maximum number of bytes a call to process() + * could produce for the given number of input bytes. This may + * be an overestimate. + */ + abstract fun maxOutputSize(len: Int): Int + } + + /* package */ + internal class Decoder(flags: Int, output: ByteArray?) : Coder() { + /** + * States 0-3 are reading through the next input tuple. + * State 4 is having read one '=' and expecting exactly + * one more. + * State 5 is expecting no more data or padding characters + * in the input. + * State 6 is the error state; an error has been detected + * in the input and no future input can "fix" it. + */ + private var state: Int // state number (0 to 6) + private var value: Int + + private val alphabet: IntArray + + init { + this.output = output + + alphabet = if ((flags and URL_SAFE) == 0) DECODE else DECODE_WEBSAFE + state = 0 + value = 0 + } + + /** + * @return an overestimate for the number of bytes `len` bytes could decode to. + */ + override fun maxOutputSize(len: Int): Int { + return len * 3 / 4 + 10 + } + + /** + * Decode another block of input data. + * + * @return true if the state machine is still healthy. false if + * bad base-64 data has been detected in the input stream. + */ + override fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean { + var len = len + if (this.state == 6) return false + + var p = offset + len += offset + + // Using local variables makes the decoder about 12% + // faster than if we manipulate the member variables in + // the loop. (Even alphabet makes a measurable + // difference, which is somewhat surprising to me since + // the member variable is final.) + var state = this.state + var value = this.value + var op = 0 + val output = this.output + val alphabet = this.alphabet + + while (p < len) { + // Try the fast path: we're starting a new tuple and the + // next four bytes of the input stream are all data + // bytes. This corresponds to going through states + // 0-1-2-3-0. We expect to use this method for most of + // the data. + // + // If any of the next four bytes of input are non-data + // (whitespace, etc.), value will end up negative. (All + // the non-data values in decode are small negative + // numbers, so shifting any of them up and or'ing them + // together will result in a value with its top bit set.) + // + // You can remove this whole block and the output should + // be the same, just slower. + if (state == 0) { + while (p + 4 <= len && + (((alphabet[input[p].toInt() and 0xff] shl 18) or + (alphabet[input[p + 1].toInt() and 0xff] shl 12) or + (alphabet[input[p + 2].toInt() and 0xff] shl 6) or + (alphabet[input[p + 3].toInt() and 0xff])).also { value = it }) >= 0 + ) { + output?.set(op + 2, value.toByte()) + output?.set(op + 1, (value shr 8).toByte()) + output?.set(op, (value shr 16).toByte()) + op += 3 + p += 4 + } + if (p >= len) break + } + + // The fast path isn't available -- either we've read a + // partial tuple, or the next four input bytes aren't all + // data, or whatever. Fall back to the slower state + // machine implementation. + val d = alphabet[input[p++].toInt() and 0xff] + + when (state) { + 0 -> if (d >= 0) { + value = d + ++state + } else if (d != SKIP) { + this.state = 6 + return false + } + + 1 -> if (d >= 0) { + value = (value shl 6) or d + ++state + } else if (d != SKIP) { + this.state = 6 + return false + } + + 2 -> if (d >= 0) { + value = (value shl 6) or d + ++state + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect exactly one more padding character. + output?.set(op++, (value shr 4).toByte()) + state = 4 + } else if (d != SKIP) { + this.state = 6 + return false + } + + 3 -> if (d >= 0) { + // Emit the output triple and return to state 0. + value = (value shl 6) or d + output?.set(op + 2, value.toByte()) + output?.set(op + 1, (value shr 8).toByte()) + output?.set(op, (value shr 16).toByte()) + op += 3 + state = 0 + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect no further data or padding characters. + output?.set(op + 1, (value shr 2).toByte()) + output?.set(op, (value shr 10).toByte()) + op += 2 + state = 5 + } else if (d != SKIP) { + this.state = 6 + return false + } + + 4 -> if (d == EQUALS) { + ++state + } else if (d != SKIP) { + this.state = 6 + return false + } + + 5 -> if (d != SKIP) { + this.state = 6 + return false + } + } + } + + if (!finish) { + // We're out of input, but a future call could provide + // more. + this.state = state + this.value = value + this.op = op + return true + } + + // Done reading input. Now figure out where we are left in + // the state machine and finish up. + when (state) { + 0 -> {} + 1 -> { + // Read one extra input byte, which isn't enough to + // make another output byte. Illegal. + this.state = 6 + return false + } + + 2 -> // Read two extra input bytes, enough to emit 1 more + // output byte. Fine. + output?.set(op++, (value shr 4).toByte()) + + 3 -> { + // Read three extra input bytes, enough to emit 2 more + // output bytes. Fine. + output?.set(op++, (value shr 10).toByte()) + output?.set(op++, (value shr 2).toByte()) + } + + 4 -> { + // Read one padding '=' when we expected 2. Illegal. + this.state = 6 + return false + } + + 5 -> {} + } + + this.state = state + this.op = op + return true + } + + companion object { + /** + * Lookup table for turning bytes into their position in the + * Base64 alphabet. + */ + private val DECODE = intArrayOf( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + ) + + /** + * Decode lookup table for the "web safe" variant (RFC 3548 + * sec. 4) where - and _ replace + and /. + */ + private val DECODE_WEBSAFE = intArrayOf( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + ) + + /** + * Non-data values in the DECODE arrays. + */ + private const val SKIP = -1 + private const val EQUALS = -2 + } + } + + /* package */ + internal class Encoder(flags: Int, output: ByteArray?) : Coder() { + private val tail: ByteArray + + /* package */ + var tailLen: Int + private var count: Int + + val do_padding: Boolean + val do_newline: Boolean + val do_cr: Boolean + private val alphabet: ByteArray + + init { + this.output = output + + do_padding = (flags and NO_PADDING) == 0 + do_newline = (flags and NO_WRAP) == 0 + do_cr = (flags and CRLF) != 0 + alphabet = if ((flags and URL_SAFE) == 0) ENCODE else ENCODE_WEBSAFE + + tail = ByteArray(2) + tailLen = 0 + + count = if (do_newline) LINE_GROUPS else -1 + } + + /** + * @return an overestimate for the number of bytes `len` bytes could encode to. + */ + override fun maxOutputSize(len: Int): Int { + return len * 8 / 5 + 10 + } + + override fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean { + // Using local variables makes the encoder about 9% faster. + var len = len + val alphabet = this.alphabet + val output = this.output + var op = 0 + var count = this.count + + var p = offset + len += offset + var v = -1 + + // First we need to concatenate the tail of the previous call + // with any input bytes available now and see if we can empty + // the tail. + when (tailLen) { + 0 -> {} + 1 -> { + if (p + 2 <= len) { + // A 1-byte tail with at least 2 bytes of + // input available now. + v = ((tail[0].toInt() and 0xff) shl 16) or + ((input[p++].toInt() and 0xff) shl 8) or + (input[p++].toInt() and 0xff) + tailLen = 0 + } + } + + 2 -> if (p + 1 <= len) { + // A 2-byte tail with at least 1 byte of input. + v = ((tail[0].toInt() and 0xff) shl 16) or + ((tail[1].toInt() and 0xff) shl 8) or + (input[p++].toInt() and 0xff) + tailLen = 0 + } + } + + if (v != -1) { + output?.set(op++, alphabet[(v shr 18) and 0x3f]) + output?.set(op++, alphabet[(v shr 12) and 0x3f]) + output?.set(op++, alphabet[(v shr 6) and 0x3f]) + output?.set(op++, alphabet[v and 0x3f]) + if (--count == 0) { + if (do_cr) output?.set(op++, '\r'.code.toByte()) + output?.set(op++, '\n'.code.toByte()) + count = LINE_GROUPS + } + } + + // At this point either there is no tail, or there are fewer + // than 3 bytes of input available. + + // The main loop, turning 3 input bytes into 4 output bytes on + // each iteration. + while (p + 3 <= len) { + v = ((input[p].toInt() and 0xff) shl 16) or + ((input[p + 1].toInt() and 0xff) shl 8) or + (input[p + 2].toInt() and 0xff) + output?.set(op, alphabet[(v shr 18) and 0x3f]) + output?.set(op + 1, alphabet[(v shr 12) and 0x3f]) + output?.set(op + 2, alphabet[(v shr 6) and 0x3f]) + output?.set(op + 3, alphabet[v and 0x3f]) + p += 3 + op += 4 + if (--count == 0) { + if (do_cr) output?.set(op++, '\r'.code.toByte()) + output?.set(op++, '\n'.code.toByte()) + count = LINE_GROUPS + } + } + + if (finish) { + // Finish up the tail of the input. Note that we need to + // consume any bytes in tail before any bytes + // remaining in input; there should be at most two bytes + // total. + + if (p - tailLen == len - 1) { + var t = 0 + v = ((if (tailLen > 0) tail[t++] else input[p++]).toInt() and 0xff) shl 4 + tailLen -= t + output?.set(op++, alphabet[(v shr 6) and 0x3f]) + output?.set(op++, alphabet[v and 0x3f]) + if (do_padding) { + output?.set(op++, '='.code.toByte()) + output?.set(op++, '='.code.toByte()) + } + if (do_newline) { + if (do_cr) output?.set(op++, '\r'.code.toByte()) + output?.set(op++, '\n'.code.toByte()) + } + } else if (p - tailLen == len - 2) { + var t = 0 + v = (((if (tailLen > 1) tail[t++] else input[p++]).toInt() and 0xff) shl 10) or + (((if (tailLen > 0) tail[t++] else input[p++]).toInt() and 0xff) shl 2) + tailLen -= t + output?.set(op++, alphabet[(v shr 12) and 0x3f]) + output?.set(op++, alphabet[(v shr 6) and 0x3f]) + output?.set(op++, alphabet[v and 0x3f]) + if (do_padding) { + output?.set(op++, '='.code.toByte()) + } + if (do_newline) { + if (do_cr) output?.set(op++, '\r'.code.toByte()) + output?.set(op++, '\n'.code.toByte()) + } + } else if (do_newline && op > 0 && count != LINE_GROUPS) { + if (do_cr) output?.set(op++, '\r'.code.toByte()) + output?.set(op++, '\n'.code.toByte()) + } + + assert(tailLen == 0) + assert(p == len) + } else { + // Save the leftovers in tail to be consumed on the next + // call to encodeInternal. + + if (p == len - 1) { + tail[tailLen++] = input[p] + } else if (p == len - 2) { + tail[tailLen++] = input[p] + tail[tailLen++] = input[p + 1] + } + } + + this.op = op + this.count = count + + return true + } + + companion object { + /** + * Emit a new line every this many output tuples. Corresponds to + * a 76-character line length (the maximum allowable according to + * [RFC 2045](http://www.ietf.org/rfc/rfc2045.txt)). + */ + const val LINE_GROUPS: Int = 19 + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private val ENCODE = byteArrayOf( + 'A'.code.toByte(), + 'B'.code.toByte(), + 'C'.code.toByte(), + 'D'.code.toByte(), + 'E'.code.toByte(), + 'F'.code.toByte(), + 'G'.code.toByte(), + 'H'.code.toByte(), + 'I'.code.toByte(), + 'J'.code.toByte(), + 'K'.code.toByte(), + 'L'.code.toByte(), + 'M'.code.toByte(), + 'N'.code.toByte(), + 'O'.code.toByte(), + 'P'.code.toByte(), + 'Q'.code.toByte(), + 'R'.code.toByte(), + 'S'.code.toByte(), + 'T'.code.toByte(), + 'U'.code.toByte(), + 'V'.code.toByte(), + 'W'.code.toByte(), + 'X'.code.toByte(), + 'Y'.code.toByte(), + 'Z'.code.toByte(), + 'a'.code.toByte(), + 'b'.code.toByte(), + 'c'.code.toByte(), + 'd'.code.toByte(), + 'e'.code.toByte(), + 'f'.code.toByte(), + 'g'.code.toByte(), + 'h'.code.toByte(), + 'i'.code.toByte(), + 'j'.code.toByte(), + 'k'.code.toByte(), + 'l'.code.toByte(), + 'm'.code.toByte(), + 'n'.code.toByte(), + 'o'.code.toByte(), + 'p'.code.toByte(), + 'q'.code.toByte(), + 'r'.code.toByte(), + 's'.code.toByte(), + 't'.code.toByte(), + 'u'.code.toByte(), + 'v'.code.toByte(), + 'w'.code.toByte(), + 'x'.code.toByte(), + 'y'.code.toByte(), + 'z'.code.toByte(), + '0'.code.toByte(), + '1'.code.toByte(), + '2'.code.toByte(), + '3'.code.toByte(), + '4'.code.toByte(), + '5'.code.toByte(), + '6'.code.toByte(), + '7'.code.toByte(), + '8'.code.toByte(), + '9'.code.toByte(), + '+'.code.toByte(), + '/'.code.toByte(), + ) + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private val ENCODE_WEBSAFE = byteArrayOf( + 'A'.code.toByte(), + 'B'.code.toByte(), + 'C'.code.toByte(), + 'D'.code.toByte(), + 'E'.code.toByte(), + 'F'.code.toByte(), + 'G'.code.toByte(), + 'H'.code.toByte(), + 'I'.code.toByte(), + 'J'.code.toByte(), + 'K'.code.toByte(), + 'L'.code.toByte(), + 'M'.code.toByte(), + 'N'.code.toByte(), + 'O'.code.toByte(), + 'P'.code.toByte(), + 'Q'.code.toByte(), + 'R'.code.toByte(), + 'S'.code.toByte(), + 'T'.code.toByte(), + 'U'.code.toByte(), + 'V'.code.toByte(), + 'W'.code.toByte(), + 'X'.code.toByte(), + 'Y'.code.toByte(), + 'Z'.code.toByte(), + 'a'.code.toByte(), + 'b'.code.toByte(), + 'c'.code.toByte(), + 'd'.code.toByte(), + 'e'.code.toByte(), + 'f'.code.toByte(), + 'g'.code.toByte(), + 'h'.code.toByte(), + 'i'.code.toByte(), + 'j'.code.toByte(), + 'k'.code.toByte(), + 'l'.code.toByte(), + 'm'.code.toByte(), + 'n'.code.toByte(), + 'o'.code.toByte(), + 'p'.code.toByte(), + 'q'.code.toByte(), + 'r'.code.toByte(), + 's'.code.toByte(), + 't'.code.toByte(), + 'u'.code.toByte(), + 'v'.code.toByte(), + 'w'.code.toByte(), + 'x'.code.toByte(), + 'y'.code.toByte(), + 'z'.code.toByte(), + '0'.code.toByte(), + '1'.code.toByte(), + '2'.code.toByte(), + '3'.code.toByte(), + '4'.code.toByte(), + '5'.code.toByte(), + '6'.code.toByte(), + '7'.code.toByte(), + '8'.code.toByte(), + '9'.code.toByte(), + '-'.code.toByte(), + '_'.code.toByte(), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/pseudo/Constants.kt b/app/src/main/java/com/android/grape/pseudo/Constants.kt new file mode 100644 index 0000000..23e0537 --- /dev/null +++ b/app/src/main/java/com/android/grape/pseudo/Constants.kt @@ -0,0 +1,8 @@ +package com.android.grape.pseudo + +internal object Constants { + const val LINE_ENDING: String = "\r\n" + val GENERATOR_NAME: String = + java.lang.String.format("PseudoApkSigner %s", "1.0") + const val UTF8: String = "UTF-8" +} diff --git a/app/src/main/java/com/android/grape/pseudo/ManifestBuilder.kt b/app/src/main/java/com/android/grape/pseudo/ManifestBuilder.kt new file mode 100644 index 0000000..bd3e640 --- /dev/null +++ b/app/src/main/java/com/android/grape/pseudo/ManifestBuilder.kt @@ -0,0 +1,71 @@ +package com.android.grape.pseudo + + +internal class ManifestBuilder { + private val mEntries: ArrayList + + private var mVersion: Long = 0 + private var mCachedManifest: String = "" + private var mCachedVersion: Long = -1 + + init { + mEntries = ArrayList() + } + + fun build(): String { + if (mVersion == mCachedVersion) return mCachedManifest + + val stringBuilder = StringBuilder() + + stringBuilder.append(generateHeader().toString()) + for (entry in mEntries) { + stringBuilder.append(entry.toString()) + } + + mCachedVersion = mVersion + mCachedManifest = stringBuilder.toString() + + return mCachedManifest + } + + private fun generateHeader(): ManifestEntry { + val header = ManifestEntry() + header.setAttribute("Manifest-Version", "1.0") + header.setAttribute("Created-By", Constants.GENERATOR_NAME) + return header + } + + internal class ManifestEntry { + private val mAttributes = LinkedHashMap() + + fun setAttribute(attribute: String, value: String) { + mAttributes[attribute] = value + } + + fun getAttribute(attribute: String): String? { + return mAttributes[attribute] + } + + override fun toString(): String { + val stringBuilder = StringBuilder() + + for (key in mAttributes.keys) stringBuilder.append( + String.format( + "%s: %s" + Constants.LINE_ENDING, key, + mAttributes[key] + ) + ) + stringBuilder.append(Constants.LINE_ENDING) + + return stringBuilder.toString() + } + } + + fun addEntry(entry: ManifestEntry) { + mEntries.add(entry) + mVersion++ + } + + val entries: List + get() = mEntries +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/pseudo/PseudoApkSigner.kt b/app/src/main/java/com/android/grape/pseudo/PseudoApkSigner.kt new file mode 100644 index 0000000..ff47f4d --- /dev/null +++ b/app/src/main/java/com/android/grape/pseudo/PseudoApkSigner.kt @@ -0,0 +1,114 @@ +package com.android.grape.pseudo + +import android.provider.SyncStateContract +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.security.DigestInputStream +import java.security.MessageDigest +import java.security.interfaces.RSAPrivateKey +import java.util.Locale +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + + +class PseudoApkSigner(private val mTemplateFile: File, privateKey: File) { + private val mPrivateKey: RSAPrivateKey = Utils.readPrivateKey(privateKey) + + private var mSignerName = "CERT" + + @Throws(Exception::class) + fun sign(apkFile: File, output: File) { + sign(FileInputStream(apkFile), FileOutputStream(output)) + } + + @Throws(Exception::class) + fun sign(apkInputStream: InputStream, output: OutputStream) { + val manifest: ManifestBuilder = ManifestBuilder() + val signature: SignatureFileGenerator = SignatureFileGenerator(manifest, HASHING_ALGORITHM) + + val apkZipInputStream = ZipInputStream(apkInputStream) + + val zipOutputStream: ZipAlignZipOutputStream = ZipAlignZipOutputStream.create(output, 4) + val messageDigest = MessageDigest.getInstance(HASHING_ALGORITHM) + var zipEntry: ZipEntry + OUTER@ while ((apkZipInputStream.nextEntry.also { zipEntry = it }) != null) { + if (zipEntry.isDirectory) continue + + if (zipEntry.name.lowercase(Locale.getDefault()).startsWith("meta-inf/")) { + for (fileToSkipEnding in META_INF_FILES_TO_SKIP_ENDINGS) { + if (zipEntry.name.lowercase(Locale.getDefault()).endsWith( + fileToSkipEnding + ) + ) continue@OUTER + } + } + + + messageDigest.reset() + val entryInputStream = DigestInputStream(apkZipInputStream, messageDigest) + + val newZipEntry = ZipEntry(zipEntry.name) + newZipEntry.method = zipEntry.method + if (zipEntry.method == ZipEntry.STORED) { + newZipEntry.size = zipEntry.size + newZipEntry.compressedSize = zipEntry.size + newZipEntry.crc = zipEntry.crc + } + + zipOutputStream.alignment = if (newZipEntry.name.endsWith(".so")) 4096 else 4 + zipOutputStream.putNextEntry(newZipEntry) + Utils.copyStream(entryInputStream, zipOutputStream) + zipOutputStream.closeEntry() + apkZipInputStream.closeEntry() + + val manifestEntry: ManifestBuilder.ManifestEntry = ManifestBuilder.ManifestEntry() + manifestEntry.setAttribute("Name", zipEntry.name) + manifestEntry.setAttribute( + HASHING_ALGORITHM + "-Digest", + Utils.base64Encode(messageDigest.digest()) + ) + manifest.addEntry(manifestEntry) + } + + zipOutputStream.putNextEntry(ZipEntry("META-INF/MANIFEST.MF")) + zipOutputStream.write(manifest.build().toByteArray()) + zipOutputStream.closeEntry() + + zipOutputStream.putNextEntry(ZipEntry(String.format("META-INF/%s.SF", mSignerName))) + zipOutputStream.write(signature.generate().toByteArray()) + zipOutputStream.closeEntry() + + zipOutputStream.putNextEntry(ZipEntry(String.format("META-INF/%s.RSA", mSignerName))) + zipOutputStream.write(Utils.readFile(mTemplateFile)) + zipOutputStream.write( + Utils.sign( + HASHING_ALGORITHM, + mPrivateKey, + signature.generate().toByteArray() + ) + ) + zipOutputStream.closeEntry() + + apkZipInputStream.close() + zipOutputStream.close() + } + + + /** + * Sets name of the .SF and .RSA file in META-INF + * + * @param signerName desired .SF and .RSA files name + */ + fun setSignerName(signerName: String) { + mSignerName = signerName + } + + companion object { + private val META_INF_FILES_TO_SKIP_ENDINGS = + arrayOf("manifest.mf", ".sf", ".rsa", ".dsa", ".ec") + private const val HASHING_ALGORITHM = "SHA1" + } +} diff --git a/app/src/main/java/com/android/grape/pseudo/SignatureFileGenerator.kt b/app/src/main/java/com/android/grape/pseudo/SignatureFileGenerator.kt new file mode 100644 index 0000000..fe42a82 --- /dev/null +++ b/app/src/main/java/com/android/grape/pseudo/SignatureFileGenerator.kt @@ -0,0 +1,47 @@ +package com.android.grape.pseudo + +import com.android.grape.pseudo.Utils.base64Encode +import com.android.grape.pseudo.Utils.hash + +internal class SignatureFileGenerator( + private val mManifest: ManifestBuilder, + private val mHashingAlgorithm: String +) { + @Throws(Exception::class) + fun generate(): String { + val stringBuilder = StringBuilder() + stringBuilder.append(generateHeader().toString()) + + for (manifestEntry in mManifest.entries) { + val sfEntry = ManifestBuilder.ManifestEntry() + sfEntry.setAttribute("Name", manifestEntry.getAttribute("Name")!!) + sfEntry.setAttribute( + "$mHashingAlgorithm-Digest", base64Encode( + hash( + manifestEntry.toString().toByteArray( + charset(Constants.UTF8) + ), mHashingAlgorithm + ) + ) + ) + stringBuilder.append(sfEntry.toString()) + } + + return stringBuilder.toString() + } + + @Throws(Exception::class) + private fun generateHeader(): ManifestBuilder.ManifestEntry { + val header = ManifestBuilder.ManifestEntry() + header.setAttribute("Signature-Version", "1.0") + header.setAttribute("Created-By", Constants.GENERATOR_NAME) + header.setAttribute( + "$mHashingAlgorithm-Digest-Manifest", base64Encode( + hash( + mManifest.build()!!.toByteArray(charset(Constants.UTF8)), mHashingAlgorithm + ) + ) + ) + return header + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/pseudo/Utils.kt b/app/src/main/java/com/android/grape/pseudo/Utils.kt new file mode 100644 index 0000000..8a3b171 --- /dev/null +++ b/app/src/main/java/com/android/grape/pseudo/Utils.kt @@ -0,0 +1,84 @@ +package com.android.grape.pseudo + +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.security.KeyFactory +import java.security.MessageDigest +import java.security.PrivateKey +import java.security.Signature +import java.security.interfaces.RSAPrivateKey +import java.security.spec.PKCS8EncodedKeySpec + + +object Utils { + @Throws(Exception::class) + fun getFileHash(file: File?, hashingAlgorithm: String): ByteArray { + return getFileHash(FileInputStream(file), hashingAlgorithm) + } + + @Throws(Exception::class) + fun getFileHash(fileInputStream: InputStream, hashingAlgorithm: String): ByteArray { + val messageDigest = MessageDigest.getInstance(hashingAlgorithm) + + val buffer = ByteArray(1024 * 1024) + + var read: Int + while ((fileInputStream.read(buffer).also { read = it }) > 0) messageDigest.update( + buffer, + 0, + read + ) + + fileInputStream.close() + + return messageDigest.digest() + } + + @Throws(Exception::class) + fun hash(bytes: ByteArray, hashingAlgorithm: String): ByteArray { + val messageDigest = MessageDigest.getInstance(hashingAlgorithm) + messageDigest.update(bytes) + return messageDigest.digest() + } + + fun base64Encode(bytes: ByteArray): String { + return Base64.encodeToString(bytes, 0) + } + + @Throws(IOException::class) + fun copyStream(from: InputStream, to: OutputStream) { + val buf = ByteArray(1024 * 1024) + var len: Int + while ((from.read(buf).also { len = it }) > 0) { + to.write(buf, 0, len) + } + } + + @Throws(Exception::class) + fun sign(hashingAlgorithm: String, privateKey: PrivateKey?, message: ByteArray?): ByteArray { + val sign = Signature.getInstance(hashingAlgorithm + "withRSA") + sign.initSign(privateKey) + sign.update(message) + return sign.sign() + } + + @Throws(Exception::class) + fun readPrivateKey(file: File): RSAPrivateKey { + val keySpec = PKCS8EncodedKeySpec(readFile(file)) + return KeyFactory.getInstance("RSA").generatePrivate(keySpec) as RSAPrivateKey + } + + @Throws(IOException::class) + fun readFile(file: File): ByteArray { + val fileBytes = ByteArray(file.length().toInt()) + + val inputStream = FileInputStream(file) + inputStream.read(fileBytes) + inputStream.close() + + return fileBytes + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/pseudo/ZipAlignZipOutputStream.kt b/app/src/main/java/com/android/grape/pseudo/ZipAlignZipOutputStream.kt new file mode 100644 index 0000000..8172630 --- /dev/null +++ b/app/src/main/java/com/android/grape/pseudo/ZipAlignZipOutputStream.kt @@ -0,0 +1,64 @@ +package com.android.grape.pseudo + +import java.io.IOException +import java.io.OutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + + +class ZipAlignZipOutputStream private constructor( + outputStream: BytesCounterOutputStream, + var alignment: Int +) : + ZipOutputStream(outputStream) { + private var mBytesCounter: BytesCounterOutputStream? = null + + @Throws(IOException::class) + override fun putNextEntry(zipEntry: ZipEntry) { + if (zipEntry.method == ZipEntry.STORED) { + var headerSize = 30 + headerSize += zipEntry.name.toByteArray().size + + val temp = ((mBytesCounter?.bytesWritten?.plus(headerSize))?.rem(alignment))?:0 + val requiredPadding = + (alignment - temp).toInt() + zipEntry.extra = ByteArray(requiredPadding) + } + + super.putNextEntry(zipEntry) + } + + private class BytesCounterOutputStream(private val mWrappedOutputStream: OutputStream) : + OutputStream() { + var bytesWritten: Long = 0 + private set + + @Throws(IOException::class) + override fun write(b: ByteArray) { + mWrappedOutputStream.write(b) + bytesWritten += b.size.toLong() + } + + @Throws(IOException::class) + override fun write(b: Int) { + mWrappedOutputStream.write(b) + bytesWritten++ + } + + @Throws(IOException::class) + override fun write(b: ByteArray, off: Int, len: Int) { + mWrappedOutputStream.write(b, off, len) + bytesWritten += len.toLong() + } + } + + companion object { + fun create(outputStream: OutputStream, alignment: Int): ZipAlignZipOutputStream { + val bytesCounterOutputStream = BytesCounterOutputStream(outputStream) + val zipAlignZipOutputStream = + ZipAlignZipOutputStream(bytesCounterOutputStream, alignment) + zipAlignZipOutputStream.mBytesCounter = bytesCounterOutputStream + return zipAlignZipOutputStream + } + } +} diff --git a/app/src/main/java/com/android/grape/receiver/ScriptReceiver.kt b/app/src/main/java/com/android/grape/receiver/ScriptReceiver.kt new file mode 100644 index 0000000..a9624da --- /dev/null +++ b/app/src/main/java/com/android/grape/receiver/ScriptReceiver.kt @@ -0,0 +1,40 @@ +package com.android.grape.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.android.grape.job.UnInstallService +import com.android.grape.util.ScriptUtil +import com.android.grape.util.Util + +class ScriptReceiver : BroadcastReceiver() { + var hook_action: String = "com.android.task.board" + var EVENT: String = "event" + + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + Log.i(ScriptUtil.TAG, context.packageName + " Receive action:" + action) + val event = intent.getIntExtra(ScriptUtil.EVENT, -1) + when (event) { + ScriptUtil.START -> Log.i(ScriptUtil.TAG, "script start run...") + ScriptUtil.STOP -> { + Log.i(ScriptUtil.TAG, "proxyme revice script stop!") + val hook_intent = Intent(this.hook_action) + hook_intent.putExtra(this.EVENT, 0) + context.sendBroadcast(hook_intent) + Util.script_status = 0 + Log.i( + "IOSTQ:脚本结束", + context.packageName + " send broadcast:" + this.hook_action + ":" + this.EVENT + ) + Handler(Looper.getMainLooper()).postDelayed( + { UnInstallService.onEvent(context) }, + 3000 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/ApkSourceBuilder.kt b/app/src/main/java/com/android/grape/sai/ApkSourceBuilder.kt new file mode 100644 index 0000000..ae9e2a4 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/ApkSourceBuilder.kt @@ -0,0 +1,142 @@ +package com.android.grape.sai + +import android.content.Context +import android.net.Uri +import com.android.grape.sai.apksource.CopyToFileApkSource +import com.android.grape.sai.apksource.FilterApkSource +import com.android.grape.sai.apksource.SignerApkSource +import com.android.grape.sai.filedescriptor.ContentUriFileDescriptor +import com.android.grape.sai.filedescriptor.DefaultApkSource +import com.android.grape.sai.filedescriptor.FileDescriptor +import com.android.grape.sai.filedescriptor.NormalFileDescriptor +import com.android.grape.sai.filedescriptor.ZipApkSource +import com.android.grape.sai.filedescriptor.ZipBackedApkSource +import com.android.grape.sai.filedescriptor.ZipFileApkSource +import com.android.grape.sai.inter.ApkSource +import java.io.File + +class ApkSourceBuilder(private val mContext: Context) { + private var mSourceSet = false + private var mApkFiles: List? = null + private var mZipFile: File? = null + private var mZipUri: Uri? = null + private var mApkUris: List? = null + + private var mSigningEnabled = false + private var mZipExtractionEnabled = false + private var mReadZipViaZipFileEnabled = false + + private var mFilteredApks: Set? = null + private var mBlacklist = false + + fun fromApkFiles(apkFiles: List?): ApkSourceBuilder { + ensureSourceSetOnce() + mApkFiles = apkFiles + return this + } + + fun fromZipFile(zipFile: File?): ApkSourceBuilder { + ensureSourceSetOnce() + mZipFile = zipFile + return this + } + + fun fromZipContentUri(zipUri: Uri?): ApkSourceBuilder { + ensureSourceSetOnce() + mZipUri = zipUri + return this + } + + fun fromApkContentUris(uris: List?): ApkSourceBuilder { + ensureSourceSetOnce() + mApkUris = uris + return this + } + + fun setSigningEnabled(enabled: Boolean): ApkSourceBuilder { + mSigningEnabled = enabled + return this + } + + fun setZipExtractionEnabled(enabled: Boolean): ApkSourceBuilder { + mZipExtractionEnabled = enabled + return this + } + + fun setReadZipViaZipFileEnabled(enabled: Boolean): ApkSourceBuilder { + mReadZipViaZipFileEnabled = enabled + return this + } + + fun filterApksByLocalPath(filteredApks: Set?, blacklist: Boolean): ApkSourceBuilder { + mFilteredApks = filteredApks + mBlacklist = blacklist + return this + } + + fun build(): ApkSource { + var apkSource: ApkSource? = null + + var sourceIsZip = false + + if (mApkFiles != null) { + val apkFileDescriptors: MutableList = ArrayList( + mApkFiles?.size?: 0 + ) + for (apkFile in mApkFiles!!) apkFileDescriptors.add(NormalFileDescriptor(apkFile)) + + apkSource = DefaultApkSource(apkFileDescriptors) + } else if (mZipFile != null) { + mZipFile?.let { + val zipBackedApkSource: ZipBackedApkSource + if (mReadZipViaZipFileEnabled) zipBackedApkSource = + ZipFileApkSource(mContext, NormalFileDescriptor(it)) + else zipBackedApkSource = ZipApkSource(mContext, NormalFileDescriptor(it)) + + apkSource = zipBackedApkSource + sourceIsZip = true + } + } else if (mZipUri != null) { + mZipUri?.let { + val zipBackedApkSource: ZipBackedApkSource + if (mReadZipViaZipFileEnabled) zipBackedApkSource = + ZipFileApkSource(mContext, ContentUriFileDescriptor(mContext, it)) + else zipBackedApkSource = + ZipApkSource(mContext, ContentUriFileDescriptor(mContext, it)) + + apkSource = zipBackedApkSource + sourceIsZip = true + } + } else if (mApkUris != null) { + val apkUriDescriptors: MutableList = ArrayList( + mApkUris?.size?: 0 + ) + for (apkUri in mApkUris!!) apkUriDescriptors.add( + ContentUriFileDescriptor( + mContext, + apkUri + ) + ) + + apkSource = DefaultApkSource(apkUriDescriptors) + } else { + throw IllegalStateException("No source set") + } + apkSource?.let { + if (mSigningEnabled) apkSource = SignerApkSource(mContext, it) + + //Signing already uses temp files, so there's not reason to use CopyToFileApkSource with it + if (mZipExtractionEnabled && sourceIsZip && !mSigningEnabled) { + apkSource = CopyToFileApkSource(mContext, it) + } + + if (mFilteredApks != null) apkSource = FilterApkSource(it, mFilteredApks?:setOf(), mBlacklist) + } + return apkSource?: throw IllegalStateException("No source set") + } + + private fun ensureSourceSetOnce() { + check(!mSourceSet) { "Source can be only be set once" } + mSourceSet = true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/BaseSaiPackageInstaller.kt b/app/src/main/java/com/android/grape/sai/BaseSaiPackageInstaller.kt new file mode 100644 index 0000000..41f0253 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/BaseSaiPackageInstaller.kt @@ -0,0 +1,77 @@ +package com.android.grape.sai + +import android.annotation.SuppressLint +import android.content.Context +import com.android.grape.sai.inter.SaiPackageInstaller +import com.android.grape.sai.inter.SaiPiSessionObserver +import com.android.grape.sai.param.SaiPiSessionParams +import com.android.grape.sai.param.SaiPiSessionState +import com.android.grape.sai.param.SaiPiSessionStatus +import com.blankj.utilcode.util.LogUtils +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentSkipListMap + +@SuppressLint("UseSparseArrays") +abstract class BaseSaiPackageInstaller protected constructor(c: Context) : + SaiPackageInstaller { + protected val context: Context = c.applicationContext + private var mLastSessionId: Long = 0 + + private val mCreatedSessions: ConcurrentHashMap = + ConcurrentHashMap() + + private val mSessionStates: ConcurrentSkipListMap = + ConcurrentSkipListMap() + + private val mObservers: MutableSet = + Collections.newSetFromMap( + ConcurrentHashMap() + ) + + override fun createSession(params: SaiPiSessionParams): String { + val sessionId = newSessionId() + mCreatedSessions[sessionId] = params + setSessionState(sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.CREATED).build()) + return sessionId + } + + override fun registerSessionObserver(observer: SaiPiSessionObserver) { + mObservers.add(observer) + } + + override fun unregisterSessionObserver(observer: SaiPiSessionObserver) { + mObservers.remove(observer) + } + + override fun getSessions():List { + return Collections.unmodifiableList( + ArrayList( + mSessionStates.values + ) + ) + } + + protected fun setSessionState(sessionId: String, state: SaiPiSessionState) { + LogUtils.d( + tag(), + String.format("%s->setSessionState(%s, %s)", javaClass.simpleName, sessionId, state) + ) + mSessionStates[sessionId] = state + Utils.onMainThread { + for (observer in mObservers) observer.onSessionStateChanged(state) + } + } + + protected fun takeCreatedSession(sessionId: String): SaiPiSessionParams? { + return mCreatedSessions.remove(sessionId) + } + + @SuppressLint("DefaultLocale") + protected fun newSessionId(): String { + val sessionId = mLastSessionId++ + return String.format("%d@%s", sessionId, javaClass.name) + } + + protected abstract fun tag(): String? +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/FlexSaiPackageInstaller.kt b/app/src/main/java/com/android/grape/sai/FlexSaiPackageInstaller.kt new file mode 100644 index 0000000..6d49020 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/FlexSaiPackageInstaller.kt @@ -0,0 +1,112 @@ +package com.android.grape.sai + +import android.annotation.SuppressLint +import android.content.Context +import com.android.grape.MainApplication +import com.android.grape.sai.inter.SaiPackageInstaller +import com.android.grape.sai.inter.SaiPiSessionObserver +import com.android.grape.sai.param.SaiPiSessionParams +import com.android.grape.sai.param.SaiPiSessionState +import com.android.grape.sai.prefers.PreferencesValues +import com.android.grape.sai.rootless.RootlessSaiPackageInstaller +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap + +class FlexSaiPackageInstaller private constructor(c: Context) : SaiPackageInstaller, + SaiPiSessionObserver { + private val mContext: Context = c.applicationContext + + private var mDefaultInstaller: SaiPackageInstaller? = null + private val mInstallers = HashMap() + private val mSessionIdToInstaller = ConcurrentHashMap() + + private val mObservers: MutableSet = Collections.newSetFromMap( + ConcurrentHashMap() + ) + + init { + addInstaller( + PreferencesValues.INSTALLER_ROOTLESS, + RootlessSaiPackageInstaller.getInstance(mContext) + ) + addInstaller( + PreferencesValues.INSTALLER_ROOTED, + RootedSaiPackageInstaller.getInstance(mContext) + ) + addInstaller( + PreferencesValues.INSTALLER_SHIZUKU, + ShizukuSaiPackageInstaller.getInstance() + ) + sInstance = this + } + + fun addInstaller(id: Int, installer: SaiPackageInstaller) { + check(!mInstallers.containsKey(id)) { "Installer with this id already added" } + + if (mDefaultInstaller == null) mDefaultInstaller = installer + + mInstallers[id] = installer + installer.registerSessionObserver(this) + } + + fun createSessionOnInstaller(installerId: Int, params: SaiPiSessionParams): String { + return createSessionOnInstaller( + mInstallers[installerId]?: throw IllegalArgumentException("Unknown installer id"), params + ) + } + + private fun createSessionOnInstaller( + installer: SaiPackageInstaller, + params: SaiPiSessionParams + ): String { + val sessionId = installer.createSession(params) + mSessionIdToInstaller[sessionId!!] = installer + return sessionId + } + + override fun createSession(params: SaiPiSessionParams): String { + return createSessionOnInstaller(mDefaultInstaller!!, params) + } + + override fun enqueueSession(sessionId: String) { + val installer = mSessionIdToInstaller.remove(sessionId) + requireNotNull(installer) { "Unknown sessionId" } + + installer.enqueueSession(sessionId) + } + + override fun registerSessionObserver(observer: SaiPiSessionObserver) { + mObservers.add(observer) + } + + override fun unregisterSessionObserver(observer: SaiPiSessionObserver) { + mObservers.remove(observer) + } + + override fun getSessions(): List + { + val sessions = ArrayList() + + for (installer in mInstallers.values) sessions.addAll(installer.getSessions()) + + sessions.sort() + + return sessions + } + + override fun onSessionStateChanged(state: SaiPiSessionState?) { + for (observer in mObservers) observer.onSessionStateChanged(state) + } + + companion object { + @SuppressLint("StaticFieldLeak") + private var sInstance: FlexSaiPackageInstaller? = null + + fun getInstance(): FlexSaiPackageInstaller { + synchronized(FlexSaiPackageInstaller::class.java) { + return sInstance + ?: FlexSaiPackageInstaller(MainApplication.instance) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/IOUtils.kt b/app/src/main/java/com/android/grape/sai/IOUtils.kt new file mode 100644 index 0000000..7b65354 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/IOUtils.kt @@ -0,0 +1,179 @@ +package com.android.grape.sai + +import android.content.Context +import android.util.Log +import java.io.BufferedReader +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.Closeable +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.io.OutputStream +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.security.DigestInputStream +import java.security.MessageDigest +import java.util.zip.CRC32 + + +object IOUtils { + private const val TAG = "IOUtils" + + @Throws(IOException::class) + fun copyStream(from: InputStream, to: OutputStream) { + val buf = ByteArray(1024 * 1024) + var len: Int + while ((from.read(buf).also { len = it }) > 0) { + to.write(buf, 0, len) + } + } + + @Throws(IOException::class) + fun copyFile(original: File?, destination: File?) { + FileInputStream(original).use { inputStream -> + FileOutputStream(destination).use { outputStream -> + copyStream(inputStream, outputStream) + } + } + } + + @Throws(IOException::class) + fun copyFileFromAssets(context: Context, assetFileName: String, destination: File?) { + context.assets.open(assetFileName).use { inputStream -> + FileOutputStream(destination).use { outputStream -> + copyStream(inputStream, outputStream) + } + } + } + + fun deleteRecursively(f: File) { + if (f.isDirectory) { + val files = f.listFiles() + if (files != null) { + for (child in files) deleteRecursively(child) + } + } + f.delete() + } + + @Throws(IOException::class) + fun calculateFileCrc32(file: File?): Long { + return calculateCrc32(FileInputStream(file)) + } + + @Throws(IOException::class) + fun calculateBytesCrc32(bytes: ByteArray?): Long { + return calculateCrc32(ByteArrayInputStream(bytes)) + } + + @Throws(IOException::class) + fun calculateCrc32(inputStream: InputStream): Long { + inputStream.use { `in` -> + val crc32 = CRC32() + val buffer = ByteArray(1024 * 1024) + var read: Int + + while ((`in`.read(buffer).also { read = it }) > 0) crc32.update(buffer, 0, read) + return crc32.value + } + } + + fun writeStreamToStringBuilder(builder: StringBuilder, inputStream: InputStream?): Thread { + val t = Thread { + try { + val buf = CharArray(1024) + var len: Int + val reader = BufferedReader(InputStreamReader(inputStream)) + while ((reader.read(buf).also { len = it }) > 0) builder.append(buf, 0, len) + + reader.close() + } catch (e: Exception) { + Log.wtf(TAG, e) + } + } + t.start() + return t + } + + /** + * Read contents of input stream to a byte array and close it + * + * @param inputStream + * @return contents of input stream + * @throws IOException + */ + @Throws(IOException::class) + fun readStream(inputStream: InputStream): ByteArray { + inputStream.use { `in` -> + return readStreamNoClose(`in`) + } + } + + @Throws(IOException::class) + fun readStream(inputStream: InputStream, charset: Charset): String { + return String(readStream(inputStream), charset) + } + + /** + * Read contents of input stream to a byte array, but don't close the stream + * + * @param inputStream + * @return contents of input stream + * @throws IOException + */ + @Throws(IOException::class) + fun readStreamNoClose(inputStream: InputStream): ByteArray { + val buffer = ByteArrayOutputStream() + copyStream(inputStream, buffer) + return buffer.toByteArray() + } + + fun closeSilently(closeable: Closeable?) { + if (closeable == null) return + + try { + closeable.close() + } catch (e: Exception) { + Log.w(TAG, String.format("Unable to close %s", closeable.javaClass.canonicalName), e) + } + } + + /** + * Hashes stream content using passed [MessageDigest], closes the stream and returns digest bytes + * + * @param inputStream + * @param messageDigest + * @return + * @throws IOException + */ + @Throws(IOException::class) + fun hashStream(inputStream: InputStream?, messageDigest: MessageDigest): ByteArray { + DigestInputStream(inputStream, messageDigest).use { digestInputStream -> + val buffer = ByteArray(1024 * 64) + var read: Int + while ((digestInputStream.read(buffer).also { read = it }) > 0) { + //Do nothing + } + return messageDigest.digest() + } + } + + @Throws(IOException::class) + fun hashString(s: String, messageDigest: MessageDigest): ByteArray { + return hashStream( + ByteArrayInputStream(s.toByteArray(StandardCharsets.UTF_8)), + messageDigest + ) + } + + @Throws(IOException::class) + fun readFile(file: File?): ByteArray { + FileInputStream(file).use { `in` -> + return readStream(`in`) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/MyBroadcastReceiver.kt b/app/src/main/java/com/android/grape/sai/MyBroadcastReceiver.kt new file mode 100644 index 0000000..3151fc6 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/MyBroadcastReceiver.kt @@ -0,0 +1,56 @@ +package com.android.grape.sai + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.widget.Toast + + +/** + * @author litianxiang + * @description: + * @date :2021/6/21 18:23 + */ +class MyBroadcastReceiver : BroadcastReceiver() { + private var mReceiver: MyReceiver? = null + private var fruit: String? = null + override fun onReceive(context: Context, intent: Intent) { + //接收广播消息 + fruit = intent.getStringExtra("fruit") + //调用接口MyReceiver里面的interFruit方法传入接收的内容 + mReceiver!!.interFruit(fruit) + //使用Toast显示广播消息 + Toast.makeText(context, fruit, Toast.LENGTH_SHORT).show() + } + + //创建一个接口把接收到的广播内容传递回MainActivity + interface MyReceiver { + fun interFruit(fruit: String?) + } + + fun MyThis(mr: MyReceiver?) { + mReceiver = mr + } + + + fun sendSuccess(intent: Intent, context: Context) { + // 发送广播 + var intent = intent + intent = Intent("myBroadCast") + //android版本为8以上的,静态声明广播注册需要设置包名 + intent.setPackage("com.aefyr.sai.fdroid") + intent.putExtra("fruit", "Installation success") + context.sendBroadcast(intent) + } + + fun sendFailed(intent: Intent, context: Context) { + // 发送广播 + + var intent = intent + intent = Intent("myBroadCast") + //android版本为8以上的,静态声明广播注册需要设置包名 + intent.setPackage("com.aefyr.sai.fdroid") + intent.putExtra("fruit", "Installation failed") + context.sendBroadcast(intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/RootedSaiPackageInstaller.kt b/app/src/main/java/com/android/grape/sai/RootedSaiPackageInstaller.kt new file mode 100644 index 0000000..ffc4cd0 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/RootedSaiPackageInstaller.kt @@ -0,0 +1,41 @@ +package com.android.grape.sai + +import android.annotation.SuppressLint +import android.content.Context +import com.android.grape.MainApplication +import com.android.grape.R +import com.android.grape.sai.shell.Shell +import com.android.grape.sai.shell.SuShell + + +class RootedSaiPackageInstaller private constructor(c: Context) : + ShellSaiPackageInstaller(c) { + init { + sInstance = this + } + + override val shell: Shell + get() = SuShell.instance + + override val installerName: String + get() = "Rooted" + + override val shellUnavailableMessage: String + get() = MainApplication.instance.getString(R.string.installer_error_root_no_root) + + override fun tag(): String { + return "RootedSaiPi" + } + + companion object { + @SuppressLint("StaticFieldLeak") + private var sInstance: RootedSaiPackageInstaller? = null + + fun getInstance(c: Context): RootedSaiPackageInstaller { + synchronized(RootedSaiPackageInstaller::class.java) { + return sInstance + ?: RootedSaiPackageInstaller(c) + } + } + } +} diff --git a/app/src/main/java/com/android/grape/sai/RootlessSaiPiBroadcastReceiver.kt b/app/src/main/java/com/android/grape/sai/RootlessSaiPiBroadcastReceiver.kt new file mode 100644 index 0000000..f9db119 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/RootlessSaiPiBroadcastReceiver.kt @@ -0,0 +1,188 @@ +package com.android.grape.sai + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInstaller +import android.util.Log +import android.widget.Toast +import com.android.grape.R +import com.android.grape.sai.rootless.AndroidPackageInstallerError +import com.android.grape.sai.rootless.ConfirmationIntentWrapperActivity2 + +class RootlessSaiPiBroadcastReceiver(c: Context) : BroadcastReceiver() { + private val mContext: Context = c.applicationContext + + private val mObservers = HashSet() + + private var myBroadcastReceiver: MyBroadcastReceiver? = null + + fun addEventObserver(observer: EventObserver) { + mObservers.add(observer) + } + + fun removeEventObserver(observer: EventObserver) { + mObservers.remove(observer) + } + + override fun onReceive(context: Context, intent: Intent) { + val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999) + when (status) { + PackageInstaller.STATUS_PENDING_USER_ACTION -> { + Log.d(TAG, "Requesting user confirmation for installation") + dispatchOnConfirmationPending( + intent.getIntExtra( + PackageInstaller.EXTRA_SESSION_ID, + -1 + ), intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) + ) + val confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT) + + ConfirmationIntentWrapperActivity2.start( + context, + intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1), + confirmationIntent + ) + } + + PackageInstaller.STATUS_SUCCESS -> { + Log.d(TAG, "Installation succeed") + dispatchOnInstallationSucceeded( + intent.getIntExtra( + PackageInstaller.EXTRA_SESSION_ID, + -1 + ), intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) + ) + myBroadcastReceiver = MyBroadcastReceiver().apply { + sendSuccess(intent, context) + } + Toast.makeText(context, "Installation succeed", Toast.LENGTH_SHORT).show() + } + + else -> { + Log.d(TAG, "Installation failed") + dispatchOnInstallationFailed( + intent.getIntExtra( + PackageInstaller.EXTRA_SESSION_ID, + -1 + ), parseError(intent), getRawError(intent), null + ) + myBroadcastReceiver = MyBroadcastReceiver().apply { + sendFailed(intent, context) + } + Toast.makeText(context, "Installation failed", Toast.LENGTH_SHORT).show() + } + } + } + + private fun dispatchOnConfirmationPending(sessionId: Int, packageName: String?) { + for (observer in mObservers) observer.onConfirmationPending(sessionId, packageName) + } + + private fun dispatchOnInstallationSucceeded(sessionId: Int, packageName: String?) { + for (observer in mObservers) observer.onInstallationSucceeded(sessionId, packageName) + } + + private fun dispatchOnInstallationFailed( + sessionId: Int, + shortError: String, + fullError: String?, + exception: Exception? + ) { + for (observer in mObservers) observer.onInstallationFailed( + sessionId, + shortError, + fullError, + exception + ) + } + + private fun getRawError(intent: Intent): String? { + return intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + } + + private fun parseError(intent: Intent): String { + val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999) + val otherPackage = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME) + val error = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + val errorCode = intent.getIntExtra( + ANDROID_PM_EXTRA_LEGACY_STATUS, + AndroidPackageInstallerError.UNKNOWN.legacyErrorCode + ) + + if (status == STATUS_BAD_ROM) { + return mContext.getString(R.string.installer_error_lidl_rom) + } + + val androidPackageInstallerError: AndroidPackageInstallerError = + getAndroidPmError(errorCode, error) + if (androidPackageInstallerError !== AndroidPackageInstallerError.UNKNOWN) { + return androidPackageInstallerError.getDescription(mContext) + } + + return getSimplifiedErrorDescription(status, otherPackage) + } + + fun getSimplifiedErrorDescription(status: Int, blockingPackage: String?): String { + when (status) { + PackageInstaller.STATUS_FAILURE_ABORTED -> return mContext.getString(R.string.installer_error_aborted) + + PackageInstaller.STATUS_FAILURE_BLOCKED -> { + var blocker = mContext.getString(R.string.installer_error_blocked_device) + if (blockingPackage != null) { + val appLabel = Utils.getAppLabel(mContext, blockingPackage) + if (appLabel != null) blocker = appLabel + } + return mContext.getString(R.string.installer_error_blocked, blocker) + } + + PackageInstaller.STATUS_FAILURE_CONFLICT -> return mContext.getString(R.string.installer_error_conflict) + + PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> return mContext.getString(R.string.installer_error_incompatible) + + PackageInstaller.STATUS_FAILURE_INVALID -> return mContext.getString(R.string.installer_error_bad_apks) + + PackageInstaller.STATUS_FAILURE_STORAGE -> return mContext.getString(R.string.installer_error_storage) + + STATUS_BAD_ROM -> return mContext.getString(R.string.installer_error_lidl_rom) + } + return mContext.getString(R.string.installer_error_generic) + } + + fun getAndroidPmError(legacyErrorCode: Int, error: String?): AndroidPackageInstallerError { + for (androidPackageInstallerError in AndroidPackageInstallerError.entries) { + if (androidPackageInstallerError.legacyErrorCode == legacyErrorCode || (error != null && error.startsWith( + androidPackageInstallerError.error + )) + ) return androidPackageInstallerError + } + return AndroidPackageInstallerError.UNKNOWN + } + + interface EventObserver { + fun onConfirmationPending(sessionId: Int, packageName: String?) { + } + + fun onInstallationSucceeded(sessionId: Int, packageName: String?) { + } + + fun onInstallationFailed( + sessionId: Int, + shortError: String?, + fullError: String?, + exception: Exception? + ) { + } + } + + + companion object { + private const val TAG = "RootlessSaiPiBR" + + const val ANDROID_PM_EXTRA_LEGACY_STATUS: String = "android.content.pm.extra.LEGACY_STATUS" + + val ACTION_DELIVER_PI_EVENT: String = "com.android.grape.action.RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT" + + const val STATUS_BAD_ROM: Int = -322 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/SafUtils.kt b/app/src/main/java/com/android/grape/sai/SafUtils.kt new file mode 100644 index 0000000..613e817 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/SafUtils.kt @@ -0,0 +1,100 @@ +package com.android.grape.sai + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.os.ParcelFileDescriptor +import android.provider.DocumentsContract +import androidx.documentfile.provider.DocumentFile +import com.android.grape.sai.filedescriptor.FileUtils +import java.io.File + + +object SafUtils { + private const val PATH_TREE = "tree" + + fun getRootForPath(docUri: Uri): String { + val path = DocumentsContract.getTreeDocumentId(docUri) + + val indexOfLastColon = path.lastIndexOf(':') + require(indexOfLastColon != -1) { "Given uri does not contain a colon: $docUri" } + + return path.substring(0, indexOfLastColon) + } + + fun getPathWithoutRoot(docUri: Uri): String { + val path = DocumentsContract.getTreeDocumentId(docUri) + + val indexOfLastColon = path.lastIndexOf(':') + require(indexOfLastColon != -1) { "Given uri does not contain a colon: $docUri" } + + return path.substring(indexOfLastColon + 1) + } + + fun buildChildDocumentUri(directoryUri: Uri, childDisplayName: String): Uri { + require(isTreeUri(directoryUri)) { "directoryUri must be a tree uri" } + + val rootPath = getRootForPath(directoryUri) + val directoryPath = getPathWithoutRoot(directoryUri) + + val childPath = + rootPath + ":" + directoryPath + "/" + FileUtils.buildValidFatFilename(childDisplayName) + + return DocumentsContract.buildDocumentUriUsingTree(directoryUri, childPath) + } + + /** + * Test if the given URI represents a [DocumentsContract.Document] tree. + */ + fun isTreeUri(uri: Uri): Boolean { + val paths = uri.pathSegments + return (paths.size >= 2 && PATH_TREE == paths[0]) + } + + fun docFileFromSingleUriOrFileUri(context: Context, contentUri: Uri): DocumentFile? { + if (ContentResolver.SCHEME_FILE == contentUri.scheme) { + val path = contentUri.path ?: return null + + val file = File(path) + if (file.isDirectory) return null + + return DocumentFile.fromFile(file) + } else { + return DocumentFile.fromSingleUri(context, contentUri) + } + } + + fun docFileFromTreeUriOrFileUri(context: Context, contentUri: Uri): DocumentFile? { + if (ContentResolver.SCHEME_FILE == contentUri.scheme) { + val path = contentUri.path ?: return null + + val file = File(path) + if (!file.isDirectory) return null + + return DocumentFile.fromFile(file) + } else { + return DocumentFile.fromTreeUri(context, contentUri) + } + } + + fun getFileNameFromContentUri(context: Context, contentUri: Uri): String? { + val documentFile = docFileFromSingleUriOrFileUri(context, contentUri) ?: return null + + return documentFile.name + } + + /** + * @param context + * @param contentUri + * @return file length or 0 if it's unknown + */ + fun getFileLengthFromContentUri(context: Context, contentUri: Uri): Long { + val documentFile = docFileFromSingleUriOrFileUri(context, contentUri) ?: return 0 + + return documentFile.length() + } + + fun parcelFdToFile(fd: ParcelFileDescriptor): File { + return File("/proc/self/fd/" + fd.fd) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/ShellSaiPackageInstaller.kt b/app/src/main/java/com/android/grape/sai/ShellSaiPackageInstaller.kt new file mode 100644 index 0000000..b02dba9 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/ShellSaiPackageInstaller.kt @@ -0,0 +1,389 @@ +package com.android.grape.sai + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.util.Log +import android.util.Pair +import com.android.grape.MainApplication +import com.android.grape.R +import com.android.grape.sai.inter.ApkSource +import com.android.grape.sai.param.SaiPiSessionParams +import com.android.grape.sai.param.SaiPiSessionState +import com.android.grape.sai.param.SaiPiSessionStatus +import com.android.grape.sai.prefers.DbgPreferencesHelper +import com.android.grape.sai.prefers.PreferencesHelper +import com.android.grape.sai.rootless.AndroidPackageInstallerError +import com.android.grape.sai.shell.MiuiUtils +import com.android.grape.sai.shell.Shell +import com.android.grape.util.Util +import com.blankj.utilcode.util.LogUtils +import java.io.File +import java.util.Arrays +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.Semaphore +import java.util.concurrent.atomic.AtomicBoolean +import java.util.regex.Pattern + +abstract class ShellSaiPackageInstaller protected constructor(c: Context?) : + BaseSaiPackageInstaller(c!!) { + private val mAwaitingBroadcast = AtomicBoolean(false) + + private val mExecutor: ExecutorService = Executors.newFixedThreadPool(4) + private val mWorkerThread = HandlerThread("RootlessSaiPi Worker") + private val mWorkerHandler: Handler + + private var mCurrentSessionId: String? = null + + //TODO read package from apk stream, this is too potentially inconsistent + private val mPackageInstalledBroadcastReceiver: BroadcastReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Log.d(tag(), intent.toString()) + + if (!mAwaitingBroadcast.get()) return + + mAwaitingBroadcast.set(false) + + val installedPackage: String + try { + installedPackage = intent.dataString!!.replace("package:", "") + val installerPackage: String = + context.getPackageManager().getInstallerPackageName(installedPackage)?:"" + Log.d(tag(), "installerPackage=$installerPackage") + if ("com.android.grape" != installerPackage) return + } catch (e: Exception) { + Log.wtf(tag(), e) + return + } + + mCurrentSessionId?.let { + setSessionState( + it, + SaiPiSessionState.Builder( + it, + SaiPiSessionStatus.INSTALLATION_SUCCEED + ).packageName( + installedPackage + ).resolvePackageMeta(MainApplication.instance).build() + ) + } + unlockInstallation() + // Toast.makeText(context,"Installation succeed",Toast.LENGTH_SHORT).show(); + Util.setInstallRet(true) + } + } + + init { + mWorkerThread.start() + mWorkerHandler = Handler(mWorkerThread.looper) + + val packageAddedFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED) + packageAddedFilter.addDataScheme("package") + MainApplication.instance.registerReceiver( + mPackageInstalledBroadcastReceiver, + packageAddedFilter, + null, + mWorkerHandler + ) + } + + override fun enqueueSession(sessionId: String) { + val params: SaiPiSessionParams? = takeCreatedSession(sessionId) + setSessionState( + sessionId, + SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.QUEUED).appTempName( + params?.apkSource()?.appName + ).build() + ) + mExecutor.submit { + params?.let { + install(sessionId, it) + } + } + } + + private fun install(sessionId: String, params: SaiPiSessionParams) { + lockInstallation(sessionId) + val appTempName: String? = params.apkSource().appName + setSessionState( + sessionId, + SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLING).appTempName(appTempName).build() + ) + var androidSessionId: Int? = null + try { + params.apkSource().use { apkSource -> + if (!shell.isAvailable()) { + setSessionState( + sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).error( + MainApplication.instance.getString( + R.string.installer_error_shell, + installerName, + shellUnavailableMessage + ), null + ).build() + ) + unlockInstallation() + // Toast.makeText(getContext(),"Installation failed",Toast.LENGTH_SHORT).show(); + Util.setInstallRet(false) + return + } + androidSessionId = createSession() + val path = "/sdcard/apks/" + "com.zhiliaoapp.musically" + val file = File(path) + + val files = file.listFiles() + var currentApkFile = 0 + for (f in files) { + if (f.length() <= 0) { + setSessionState( + sessionId, + SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName( + appTempName + ).error( + MainApplication.instance.getString(R.string.installer_error_unknown_apk_size), + null + ).build() + ) + unlockInstallation() + // Toast.makeText(getContext(),"Installation failed",Toast.LENGTH_SHORT).show(); + Util.setInstallRet(false) + return + } + ensureCommandSucceeded( + shell.exec( + Shell.Command( + "pm", + "install-write", + f.length().toString(), + androidSessionId.toString(), + String.format("%d.apk", currentApkFile++), + f.path + ) + ) + ) + } + + mAwaitingBroadcast.set(true) + val installationResult: Shell.Result = + shell.exec(Shell.Command("pm", "install-commit", androidSessionId.toString())) + Log.i(tag(), "installationResult:" + installationResult.isSuccessful) + if (!installationResult.isSuccessful) { + mAwaitingBroadcast.set(false) + + val shortError: String = MainApplication.instance.getString( + R.string.installer_error_shell, + installerName, """ + ${getSessionInfo(apkSource)} + + ${parseError(installationResult)} + """.trimIndent() + ) + setSessionState( + sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED) + .appTempName(appTempName) + .error( + shortError, """ + $shortError + + ${installationResult.toString()} + """.trimIndent() + ) + .build() + ) + + unlockInstallation() + + Util.setInstallRet(false) + } else { + Util.isClickRet = true + } + } + } catch (e: Exception) { + //TODO this catches resources close exception causing a crash, same in rootless installer + Log.w(tag(), e) + + if (androidSessionId != null) { + shell.exec(Shell.Command("pm", "install-abandon", androidSessionId.toString())) + } + + setSessionState( + sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED) + .appTempName(appTempName) + .error( + MainApplication.instance.getString( + R.string.installer_error_shell, + installerName, """ + ${getSessionInfo(params.apkSource())} + + ${e.localizedMessage} + """.trimIndent() + ), MainApplication.instance.getString( + R.string.installer_error_shell, + installerName, """ + ${getSessionInfo(params.apkSource())} + + ${Utils.throwableToString(e)} + """.trimIndent() + ) + ) + .build() + ) + + unlockInstallation() + + Util.setInstallRet(false) + } + } + + private fun lockInstallation(sessionId: String) { + try { + mSharedSemaphore.acquire() + } catch (e: InterruptedException) { + throw RuntimeException("wtf", e) + } + mCurrentSessionId = sessionId + } + + private fun unlockInstallation() { + mSharedSemaphore.release() + } + + private fun ensureCommandSucceeded(result: Shell.Result): String { + if (!result.isSuccessful) throw RuntimeException(result.toString()) + return result.out + } + + private fun getSessionInfo(apkSource: ApkSource): String { + var saiVersion = "???" + try { + saiVersion = MainApplication.instance.getPackageManager() + .getPackageInfo(MainApplication.instance.getPackageName(), 0).versionName?:"" + } catch (e: PackageManager.NameNotFoundException) { + Log.wtf(tag(), "Unable to get SAI version", e) + } + return java.lang.String.format( + "%s: %s %s | %s | Android %s | Using %s ApkSource implementation | SAI %s", + MainApplication.instance.getString(R.string.installer_device), + Build.BRAND, + Build.MODEL, + if (MiuiUtils.isMiui) "MIUI" else "Not MIUI", + Build.VERSION.RELEASE, + apkSource.javaClass.getSimpleName(), + saiVersion + ) + } + + @Throws(RuntimeException::class) + private fun createSession(): Int { + val installLocation: String = java.lang.String.valueOf( + PreferencesHelper.getInstance(MainApplication.instance).installLocation + ) + val commandsToAttempt: ArrayList = ArrayList() + + val customInstallCreateCommand: String? = + DbgPreferencesHelper.customInstallCreateCommand + if (customInstallCreateCommand != null) { + val args = ArrayList( + Arrays.asList( + *customInstallCreateCommand.split(" ".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray())) + val command = args.removeAt(0) + commandsToAttempt.add(Shell.Command(command, *args.toTypedArray())) + LogUtils.d(tag(), "Using custom install-create command: $customInstallCreateCommand") + } else { + commandsToAttempt.add( + Shell.Command( + "pm", + "install-create", + "-r", + "--install-location", + installLocation, + "-i", + shell.makeLiteral("com.android.grape") + ) + ) + commandsToAttempt.add( + Shell.Command( + "pm", + "install-create", + "-r", + "-i", + shell.makeLiteral("com.android.grape") + ) + ) + } + + + val attemptedCommands: MutableList> = + ArrayList>() + + for (commandToAttempt in commandsToAttempt) { + val result: Shell.Result = shell.exec(commandToAttempt) + attemptedCommands.add(Pair(commandToAttempt, result.toString())) + + if (!result.isSuccessful) { + Log.w(tag(), String.format("Command failed: %s > %s", commandToAttempt, result)) + continue + } + + val sessionId = extractSessionId(result.out) + if (sessionId != null) return sessionId + else Log.w(tag(), String.format("Command failed: %s > %s", commandToAttempt, result)) + } + + val exceptionMessage = StringBuilder("Unable to create session, attempted commands: ") + var i = 1 + for (attemptedCommand in attemptedCommands) { + exceptionMessage.append("\n\n").append(i++).append(") ==========================\n") + .append(attemptedCommand.first) + .append("\nVVVVVVVVVVVVVVVV\n") + .append(attemptedCommand.second) + } + exceptionMessage.append("\n") + + throw IllegalStateException(exceptionMessage.toString()) + } + + private fun extractSessionId(commandResult: String): Int? { + try { + val sessionIdPattern = Pattern.compile("(\\d+)") + val sessionIdMatcher = sessionIdPattern.matcher(commandResult) + sessionIdMatcher.find() + return sessionIdMatcher.group(1).toInt() + } catch (e: Exception) { + Log.w(tag(), commandResult, e) + return null + } + } + + private fun parseError(installCommitResult: Shell.Result): String { + var matchedError: AndroidPackageInstallerError = AndroidPackageInstallerError.UNKNOWN + for (error in AndroidPackageInstallerError.values()) { + if (installCommitResult.out.contains(error.error)) { + matchedError = error + break + } + } + + return matchedError.getDescription(MainApplication.instance) + } + + protected abstract val shell: Shell + + protected abstract val installerName: String? + + protected abstract val shellUnavailableMessage: String? + + abstract override fun tag(): String? + + companion object { + private val mSharedSemaphore = Semaphore(1) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/ShizukuSaiPackageInstaller.kt b/app/src/main/java/com/android/grape/sai/ShizukuSaiPackageInstaller.kt new file mode 100644 index 0000000..a1da6e1 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/ShizukuSaiPackageInstaller.kt @@ -0,0 +1,41 @@ +package com.android.grape.sai + +import android.annotation.SuppressLint +import android.content.Context +import com.android.grape.MainApplication +import com.android.grape.R +import com.android.grape.sai.shell.Shell +import com.android.grape.sai.shell.ShizukuShell + + +class ShizukuSaiPackageInstaller private constructor(c: Context) : + ShellSaiPackageInstaller(c) { + init { + sInstance = this + } + + override val shell: Shell + get() = ShizukuShell.instance + + override val installerName: String + get() = "Shizuku" + + override val shellUnavailableMessage: String + get() = MainApplication.instance.getString(R.string.installer_error_shizuku_unavailable) + + override fun tag(): String { + return "ShizukuSaiPi" + } + + companion object { + @SuppressLint("StaticFieldLeak") + private var sInstance: ShizukuSaiPackageInstaller? = null + + fun getInstance(): ShizukuSaiPackageInstaller { + synchronized(ShizukuSaiPackageInstaller::class.java) { + return sInstance + ?: ShizukuSaiPackageInstaller(MainApplication.instance) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/Stopwatch.kt b/app/src/main/java/com/android/grape/sai/Stopwatch.kt new file mode 100644 index 0000000..4998663 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/Stopwatch.kt @@ -0,0 +1,9 @@ +package com.android.grape.sai + +class Stopwatch { + private val mStart = System.currentTimeMillis() + + fun millisSinceStart(): Long { + return System.currentTimeMillis() - mStart + } +} diff --git a/app/src/main/java/com/android/grape/sai/Utils.kt b/app/src/main/java/com/android/grape/sai/Utils.kt new file mode 100644 index 0000000..5997087 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/Utils.kt @@ -0,0 +1,243 @@ +package com.android.grape.sai + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.ActivityManager +import android.app.UiModeManager +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.util.TypedValue +import android.view.inputmethod.InputMethodManager +import androidx.annotation.AttrRes +import androidx.fragment.app.Fragment +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.PrintWriter +import java.io.StringWriter +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.Locale +import java.util.UUID +import java.util.zip.ZipEntry +import kotlin.math.pow + +object Utils { + private const val TAG = "SAIUtils" + + fun getAppLabel(c: Context, packageName: String): String? { + try { + val pm = c.packageManager + val appInfo = pm.getApplicationInfo(packageName, 0) + return pm.getApplicationLabel(appInfo).toString() + } catch (e: Exception) { + return null + } + } + + fun throwableToString(throwable: Throwable): String { + val sw = StringWriter(1024) + val pw = PrintWriter(sw) + + throwable.printStackTrace(pw) + pw.close() + + return sw.toString() + } + + @SuppressLint("PrivateApi") + fun getSystemProperty(key: String?): String? { + try { + return Class.forName("android.os.SystemProperties") + .getDeclaredMethod("get", String::class.java) + .invoke(null, key) as String + } catch (e: Exception) { + Log.w("SAIUtils", "Unable to use SystemProperties.get", e) + return null + } + } + + fun copyTextToClipboard(c: Context, text: CharSequence?) { + val clipboardManager = c.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboardManager.setPrimaryClip(ClipData.newPlainText("text", text)) + } + + fun getFileNameFromZipEntry(zipEntry: ZipEntry): String { + val path = zipEntry.name + val lastIndexOfSeparator = path.lastIndexOf("/") + if (lastIndexOfSeparator == -1) return path + return path.substring(lastIndexOfSeparator + 1) + } + + fun apiIsAtLeast(sdkInt: Int): Boolean { + return Build.VERSION.SDK_INT >= sdkInt + } + + fun hideKeyboard(activity: Activity) { + val inputMethodManager = + activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(activity.window.decorView.windowToken, 0) + } + + fun hideKeyboard(fragment: Fragment) { + val activity: Activity? = fragment.activity + if (activity != null) { + hideKeyboard(activity) + return + } + + val inputMethodManager = fragment.requireContext() + .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(fragment.requireView().windowToken, 0) + } + + fun escapeFileName(name: String): String { + return name.replace("[\\\\/:*?\"<>|]".toRegex(), "_") + } + + private var sSizeDecimalFormat: DecimalFormat? = null + + fun getThemeColor(c: Context, @AttrRes attribute: Int): Int { + val typedValue = TypedValue() + c.theme.resolveAttribute(attribute, typedValue, true) + return typedValue.data + } + + private val sMainThreadHandler = Handler(Looper.getMainLooper()) + + fun onMainThread(r: Runnable) { + sMainThreadHandler.post(r) + } + + fun getExtension(fileName: String): String? { + val lastDotIndex = fileName.lastIndexOf('.') + if (lastDotIndex == -1) return null + + return fileName.substring(lastDotIndex + 1) + } + + fun getFileNameWithoutExtension(fileName: String): String? { + val lastDotIndex = fileName.lastIndexOf('.') + if (lastDotIndex == -1) return null + + return fileName.substring(0, lastDotIndex) + } + + fun softRestartApp(c: Context) { + val activityManager = c.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + for (task in activityManager.appTasks) task.finishAndRemoveTask() + + val intent = c.packageManager.getLaunchIntentForPackage(c.packageName) + c.startActivity(intent) + } + + fun hardRestartApp(c: Context) { + val activityManager = c.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + for (task in activityManager.appTasks) task.finishAndRemoveTask() + + val intent = c.packageManager.getLaunchIntentForPackage(c.packageName) + c.startActivity(intent) + System.exit(0) + } + + fun dpToPx(c: Context, dp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.toFloat(), + c.resources.displayMetrics + ).toInt() + } + + fun spToPx(c: Context, dp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + dp.toFloat(), + c.resources.displayMetrics + ).toInt() + } + + fun isTv(c: Context): Boolean { + val uiModeManager = c.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + return uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION + } + + fun getParentAs(fragment: Fragment, asClass: Class): T? { + var parent: Any? = fragment.parentFragment + if (parent == null) parent = fragment.activity + + if (asClass.isInstance(parent)) return asClass.cast(parent) + + return null + } + + /** + * Create a file within `dir` directory in app's cache directory. File will have a random name. + * Even though this method is called createTEMPfile, created file won't be deleted automatically + * + * @param context + * @param dir + * @param extension + * @return + */ + fun createTempFileInCache(context: Context, dir: String, extension: String): File? { + val directory = File(context.cacheDir, dir) + if (!directory.exists() && !directory.mkdir()) return null + + return createUniqueFileInDirectory(directory, extension) + } + + fun createUniqueFileInDirectory(dir: File, extension: String): File? { + if (!dir.exists() && !dir.mkdirs() && !dir.exists()) return null + + if (!dir.canWrite()) return null + + var file: File? = null + while (file == null || file.exists()) file = + File(dir, UUID.randomUUID().toString() + "." + extension) + + return file + } + + @Throws(Exception::class) + fun saveDrawableAsPng(drawable: Drawable, pngFile: File?) { + var bitmap: Bitmap? = null + try { + bitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + drawable.draw(canvas) + + FileOutputStream(pngFile).use { outputStream -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + } + } finally { + bitmap?.recycle() + } + } + + private val HEX_ARRAY = "0123456789ABCDEF".toCharArray() + + fun bytesToHex(bytes: ByteArray): String { + val hexChars = CharArray(bytes.size * 2) + for (j in bytes.indices) { + val v = bytes[j].toInt() and 0xFF + hexChars[j * 2] = HEX_ARRAY[v ushr 4] + hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F] + } + return String(hexChars) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/apksource/CopyToFileApkSource.kt b/app/src/main/java/com/android/grape/sai/apksource/CopyToFileApkSource.kt new file mode 100644 index 0000000..7800bb0 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/apksource/CopyToFileApkSource.kt @@ -0,0 +1,84 @@ +package com.android.grape.sai.apksource + +import android.content.Context +import com.android.grape.sai.IOUtils +import com.android.grape.sai.inter.ApkSource +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.InputStream + +/** + * An ApkSource implementation that copies APK files from the wrapped ApkSource to a temp file. Used to fix unknown APK sizes when necessary + */ +class CopyToFileApkSource(context: Context, wrappedApkSource: ApkSource) : + ApkSource { + private val mContext: Context = context.applicationContext + private val mWrappedApkSource: ApkSource = wrappedApkSource + + private var mTempDir: File? = null + private var mCurrentApkFile: File? = null + + @Throws(Exception::class) + override fun nextApk(): Boolean { + if (!mWrappedApkSource.nextApk()) return false + + if (mTempDir == null) mTempDir = createTempDir() + mCurrentApkFile?.let { + IOUtils.deleteRecursively(it) + } + + mCurrentApkFile = File(mTempDir, mWrappedApkSource.apkName) + + mWrappedApkSource.openApkInputStream().use { `in` -> + FileOutputStream(mCurrentApkFile).use { out -> + `in`?.let { + IOUtils.copyStream(`in`, out) + } + } + } + return true + } + + @Throws(Exception::class) + override fun openApkInputStream(): InputStream { + return FileInputStream(mCurrentApkFile) + } + + override val apkLength: Long + get() = mCurrentApkFile?.length()?:0 + + @get:Throws(Exception::class) + override val apkName: String? + get() = mWrappedApkSource.apkName + + @get:Throws(Exception::class) + override val apkLocalPath: String? + get() = mWrappedApkSource.apkLocalPath + + @Throws(Exception::class) + override fun close() { + var suppressedException: Exception? = null + try { + mWrappedApkSource.close() + } catch (e: Exception) { + suppressedException = e + } + + mTempDir?.let { + IOUtils.deleteRecursively(it) + } + + if (suppressedException != null) throw suppressedException + } + + override val appName: String? + get() = mWrappedApkSource.apkName + + private fun createTempDir(): File { + var tempDir = File(mContext.filesDir, "CopyToFileApkSource") + tempDir = File(tempDir, System.currentTimeMillis().toString()) + tempDir.mkdirs() + return tempDir + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/apksource/FilterApkSource.kt b/app/src/main/java/com/android/grape/sai/apksource/FilterApkSource.kt new file mode 100644 index 0000000..eb418a8 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/apksource/FilterApkSource.kt @@ -0,0 +1,56 @@ +package com.android.grape.sai.apksource + +import com.android.grape.sai.inter.ApkSource +import java.io.InputStream + + +/** + * An ApkSource that can filter out APK files from the backing ZipBackedApkSource + */ +class FilterApkSource(apkSource: ApkSource, filteredEntries: Set, blacklist: Boolean) : + ApkSource { + private val mWrappedApkSource: ApkSource = apkSource + private val mFilteredEntries = filteredEntries + private val mBlacklist = blacklist + + @Throws(Exception::class) + override fun nextApk(): Boolean { + if (!mWrappedApkSource.nextApk()) return false + + while (shouldSkip(apkLocalPath?:"")) { + if (!mWrappedApkSource.nextApk()) return false + } + + return true + } + + private fun shouldSkip(localPath: String): Boolean { + return if (mBlacklist) mFilteredEntries.contains(localPath) + else !mFilteredEntries.contains(localPath) + } + + @Throws(Exception::class) + override fun openApkInputStream(): InputStream? { + return mWrappedApkSource.openApkInputStream() + } + + @get:Throws(Exception::class) + override val apkLength: Long + get() = mWrappedApkSource.apkLength + + @get:Throws(Exception::class) + override val apkName: String? + get() = mWrappedApkSource.apkName + + @get:Throws(Exception::class) + override val apkLocalPath: String? + get() = mWrappedApkSource.apkLocalPath + + @Throws(Exception::class) + override fun close() { + mWrappedApkSource.close() + } + + override val appName: String? + get() = mWrappedApkSource.appName +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/apksource/SignerApkSource.kt b/app/src/main/java/com/android/grape/sai/apksource/SignerApkSource.kt new file mode 100644 index 0000000..c51b2dd --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/apksource/SignerApkSource.kt @@ -0,0 +1,108 @@ +package com.android.grape.sai.apksource + +import android.content.Context +import android.util.Log +import com.android.grape.pseudo.PseudoApkSigner +import com.android.grape.sai.IOUtils +import com.android.grape.sai.inter.ApkSource +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.InputStream + +class SignerApkSource(private val mContext: Context, apkSource: ApkSource) : ApkSource { + private val mWrappedApkSource: ApkSource = apkSource + private var mIsPrepared = false + private var mApkSigner: PseudoApkSigner? = null + private var mTempDir: File? = null + + private var mCurrentSignedApkFile: File? = null + + @Throws(Exception::class) + override fun nextApk(): Boolean { + if (!mWrappedApkSource.nextApk()) { + return false + } + + if (!mIsPrepared) { + checkAndPrepareSigningEnvironment() + createTempDir() + mApkSigner = PseudoApkSigner( + File(signingEnvironmentDir, FILE_NAME_PAST), File( + signingEnvironmentDir, FILE_NAME_PRIVATE_KEY + ) + ) + } + + mCurrentSignedApkFile = File(mTempDir, apkName) + mWrappedApkSource.openApkInputStream()?.let { + mApkSigner?.sign( + it, + FileOutputStream(mCurrentSignedApkFile) + ) + } + + return true + } + + @Throws(Exception::class) + override fun openApkInputStream(): InputStream { + return FileInputStream(mCurrentSignedApkFile) + } + + override val apkLength: Long + get() = mCurrentSignedApkFile!!.length() + + @get:Throws(Exception::class) + override val apkName: String? + get() = mWrappedApkSource.apkName + + @get:Throws(Exception::class) + override val apkLocalPath: String? + get() = mWrappedApkSource.apkLocalPath + + @Throws(Exception::class) + override fun close() { + mTempDir?.let { + IOUtils.deleteRecursively(it) + } + mWrappedApkSource.close() + } + + override val appName: String? + get() = mWrappedApkSource.appName + + @Throws(Exception::class) + private fun checkAndPrepareSigningEnvironment() { + val signingEnvironment = signingEnvironmentDir + val pastFile = File(signingEnvironment, FILE_NAME_PAST) + val privateKeyFile = File(signingEnvironment, FILE_NAME_PRIVATE_KEY) + + if (pastFile.exists() && privateKeyFile.exists()) { + mIsPrepared = true + return + } + + Log.d(TAG, "Preparing signing environment...") + signingEnvironment.mkdir() + + IOUtils.copyFileFromAssets(mContext, FILE_NAME_PAST, pastFile) + IOUtils.copyFileFromAssets(mContext, FILE_NAME_PRIVATE_KEY, privateKeyFile) + + mIsPrepared = true + } + + private val signingEnvironmentDir: File + get() = File(mContext.filesDir, "signing") + + private fun createTempDir() { + mTempDir = File(mContext.filesDir, System.currentTimeMillis().toString()) + mTempDir!!.mkdirs() + } + + companion object { + private const val TAG = "SignerApkSource" + private const val FILE_NAME_PAST = "testkey.past" + private const val FILE_NAME_PRIVATE_KEY = "testkey.pk8" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/filedescriptor/ContentUriFileDescriptor.kt b/app/src/main/java/com/android/grape/sai/filedescriptor/ContentUriFileDescriptor.kt new file mode 100644 index 0000000..658e5f6 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/filedescriptor/ContentUriFileDescriptor.kt @@ -0,0 +1,40 @@ +package com.android.grape.sai.filedescriptor + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import com.android.grape.sai.SafUtils +import java.io.InputStream + + +class ContentUriFileDescriptor(c: Context, private val mContentUri: Uri) : + FileDescriptor { + private val mContentResolver: ContentResolver = c.contentResolver + private val mDocumentFile: DocumentFile? = SafUtils.docFileFromSingleUriOrFileUri(c, mContentUri) + + + @Throws(Exception::class) + override fun name(): String { + val name = mDocumentFile?.name + ?: throw BadContentProviderException("DISPLAY_NAME column is null") + + return name + } + + @Throws(Exception::class) + override fun length(): Long { + val length = mDocumentFile?.length()?:0 + + if (length == 0L) throw BadContentProviderException("SIZE column is 0") + + return length + } + + @Throws(Exception::class) + override fun open(): InputStream? { + return mContentResolver.openInputStream(mContentUri) + } + + private class BadContentProviderException(message: String?) : Exception(message) +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/filedescriptor/DefaultApkSource.kt b/app/src/main/java/com/android/grape/sai/filedescriptor/DefaultApkSource.kt new file mode 100644 index 0000000..48da9ac --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/filedescriptor/DefaultApkSource.kt @@ -0,0 +1,44 @@ +package com.android.grape.sai.filedescriptor + +import android.util.Log +import com.android.grape.sai.inter.ApkSource +import java.io.InputStream + +class DefaultApkSource(private val mApkFileDescriptors: MutableList) : + ApkSource { + private var mCurrentApk: FileDescriptor? = null + + override fun nextApk(): Boolean { + if (mApkFileDescriptors.size == 0) return false + + mCurrentApk = mApkFileDescriptors.removeAt(0) + return true + } + + @Throws(Exception::class) + override fun openApkInputStream(): InputStream? { + return mCurrentApk?.open() + } + + @get:Throws(Exception::class) + override val apkLength: Long + get() = mCurrentApk?.length()?: 0 + + @get:Throws(Exception::class) + override val apkName: String? + get() = mCurrentApk?.name() + + @get:Throws(Exception::class) + override val apkLocalPath: String? + get() = mCurrentApk?.name() + + override val appName: String? + get() { + try { + return if (mApkFileDescriptors.size == 1) mApkFileDescriptors[0].name() else null + } catch (e: Exception) { + Log.w("DefaultApkSource", "Unable to get app name", e) + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/filedescriptor/FileDescriptor.kt b/app/src/main/java/com/android/grape/sai/filedescriptor/FileDescriptor.kt new file mode 100644 index 0000000..6d323de --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/filedescriptor/FileDescriptor.kt @@ -0,0 +1,15 @@ +package com.android.grape.sai.filedescriptor + +import java.io.InputStream + + +interface FileDescriptor { + @Throws(Exception::class) + fun name(): String? + + @Throws(Exception::class) + fun length(): Long + + @Throws(Exception::class) + fun open(): InputStream? +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/filedescriptor/FileUtils.kt b/app/src/main/java/com/android/grape/sai/filedescriptor/FileUtils.kt new file mode 100644 index 0000000..67b6392 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/filedescriptor/FileUtils.kt @@ -0,0 +1,52 @@ +package com.android.grape.sai.filedescriptor + +import android.text.TextUtils +import java.nio.charset.StandardCharsets + +object FileUtils { + /** + * Mutate the given filename to make it valid for a FAT filesystem, + * replacing any invalid characters with "_". + */ + fun buildValidFatFilename(name: String): String { + if (TextUtils.isEmpty(name) || "." == name || ".." == name) { + return "(invalid)" + } + val res = StringBuilder(name.length) + for (element in name) { + val c = element + if (isValidFatFilenameChar(c)) { + res.append(c) + } else { + res.append('_') + } + } + // Even though vfat allows 255 UCS-2 chars, we might eventually write to + // ext4 through a FUSE layer, so use that limit. + trimFilename(res, 255) + return res.toString() + } + + private fun isValidFatFilenameChar(c: Char): Boolean { + if ((c.code in 0x00..0x1f)) { + return false + } + return when (c) { + '"', '*', '/', ':', '<', '>', '?', '\\', '|', 0x7F.toChar() -> false + else -> true + } + } + + private fun trimFilename(res: StringBuilder, maxBytes: Int) { + var maxBytes = maxBytes + var raw = res.toString().toByteArray(StandardCharsets.UTF_8) + if (raw.size > maxBytes) { + maxBytes -= 3 + while (raw.size > maxBytes) { + res.deleteCharAt(res.length / 2) + raw = res.toString().toByteArray(StandardCharsets.UTF_8) + } + res.insert(res.length / 2, "...") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/filedescriptor/NormalFileDescriptor.kt b/app/src/main/java/com/android/grape/sai/filedescriptor/NormalFileDescriptor.kt new file mode 100644 index 0000000..d950549 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/filedescriptor/NormalFileDescriptor.kt @@ -0,0 +1,21 @@ +package com.android.grape.sai.filedescriptor + +import java.io.File +import java.io.FileInputStream +import java.io.InputStream + + +class NormalFileDescriptor(private val mFile: File) : FileDescriptor { + override fun name(): String { + return mFile.name + } + + override fun length(): Long { + return mFile.length() + } + + @Throws(Exception::class) + override fun open(): InputStream { + return FileInputStream(mFile) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/filedescriptor/ZipApkSource.kt b/app/src/main/java/com/android/grape/sai/filedescriptor/ZipApkSource.kt new file mode 100644 index 0000000..3de5820 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/filedescriptor/ZipApkSource.kt @@ -0,0 +1,124 @@ +package com.android.grape.sai.filedescriptor + +import android.content.Context +import android.util.Log +import com.android.grape.R +import com.android.grape.sai.Utils +import java.io.IOException +import java.io.InputStream +import java.util.Locale +import java.util.zip.ZipEntry +import java.util.zip.ZipException +import java.util.zip.ZipInputStream + +class ZipApkSource(private val mContext: Context, private val mZipFileDescriptor: FileDescriptor) : + ZipBackedApkSource { + private var mIsOpen = false + private var mSeenApkFiles = 0 + + private var mZipInputStream: ZipInputStream? = null + override var entry: ZipEntry? = null + private set + + private var mWrappedStream: ZipInputStreamWrapper? = null + + @Throws(Exception::class) + override fun nextApk(): Boolean { + if (!mIsOpen) { + mZipInputStream = ZipInputStream(mZipFileDescriptor.open()) + mWrappedStream = ZipInputStreamWrapper(mZipInputStream!!) + mIsOpen = true + } + + do { + try { + entry = mZipInputStream!!.nextEntry + } catch (e: ZipException) { + if (e.message == "only DEFLATED entries can have EXT descriptor") { + throw ZipException("only DEFLATED entries can have EXT descriptor") + } + throw e + } + } while (entry != null && (entry!!.isDirectory || !entry!!.name.lowercase(Locale.getDefault()) + .endsWith(".apk")) + ) + + if (entry == null) { + mZipInputStream?.close() + + require(mSeenApkFiles != 0) { mContext.getString(R.string.installer_error_zip_contains_no_apks) } + + return false + } + mSeenApkFiles++ + + return true + } + + override fun openApkInputStream(): InputStream? { + return mWrappedStream + } + + override val apkLength: Long + get() = entry?.size?:0 + + override val apkName: String + get() = entry?.let { Utils.getFileNameFromZipEntry(it) }?:"" + + @get:Throws(Exception::class) + override val apkLocalPath: String + get() = entry?.name?:"" + + @Throws(Exception::class) + override fun close() { + mZipInputStream?.let { + try { + it.close() + } catch (e: IOException) { + Log.w("ZipApkSource", "Unable to close ZipInputStream", e) + } + } + } + + override val appName: String? + get() { + try { + return mZipFileDescriptor.name() + } catch (e: Exception) { + Log.w("ZipApkSource", "Unable to get app name", e) + return null + } + } + + /** + * Wraps ZipInputStream so it can be used as seemingly multiple InputStreams that represent each file in the archive. + * Basically just calls closeEntry instead of close, so ZipInputStream itself won't be closed + */ + private class ZipInputStreamWrapper(private val mWrappedStream: ZipInputStream) : + InputStream() { + @Throws(IOException::class) + override fun available(): Int { + return mWrappedStream.available() + } + + @Throws(IOException::class) + override fun read(): Int { + return mWrappedStream.read() + } + + @Throws(IOException::class) + override fun read(b: ByteArray): Int { + return mWrappedStream.read(b) + } + + @Throws(IOException::class) + override fun read(b: ByteArray, off: Int, len: Int): Int { + return mWrappedStream.read(b, off, len) + } + + @Throws(IOException::class) + override fun close() { + mWrappedStream.closeEntry() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/filedescriptor/ZipBackedApkSource.kt b/app/src/main/java/com/android/grape/sai/filedescriptor/ZipBackedApkSource.kt new file mode 100644 index 0000000..80a97df --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/filedescriptor/ZipBackedApkSource.kt @@ -0,0 +1,15 @@ +package com.android.grape.sai.filedescriptor + +import com.android.grape.sai.inter.ApkSource +import java.util.zip.ZipEntry + + +/** + * An ApkSource backed by a zip archive + */ +interface ZipBackedApkSource : ApkSource { + /** + * @return ZipEntry for the current APK + */ + val entry: ZipEntry? +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/filedescriptor/ZipFileApkSource.kt b/app/src/main/java/com/android/grape/sai/filedescriptor/ZipFileApkSource.kt new file mode 100644 index 0000000..6642a3b --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/filedescriptor/ZipFileApkSource.kt @@ -0,0 +1,110 @@ +package com.android.grape.sai.filedescriptor + +import android.content.Context +import com.android.grape.R +import com.android.grape.sai.IOUtils +import com.android.grape.sai.Utils +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.util.Enumeration +import java.util.Locale +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +/** + * An ApkSource implementation that copies given zip file FileDescriptor to a temp file and uses [ZipFile] API to read APKs from it. + * Used to read zip archives that are not compatible with ZipInputStream. + */ +class ZipFileApkSource(context: Context, private val mZipFileDescriptor: FileDescriptor) : + ZipBackedApkSource { + private val mContext: Context = context.applicationContext + + private var mTempFile: File? = null + private var mZipFile: ZipFile? = null + + private var mZipEntries: Enumeration? = null + + override var entry: ZipEntry? = null + private set + + private var mSeenApkFile = false + + @Throws(Exception::class) + override fun nextApk(): Boolean { + if (mZipFile == null) copyAndOpenZip() + + entry = null + while (entry == null && mZipEntries!!.hasMoreElements()) { + val nextEntry = mZipEntries!!.nextElement() + if (!nextEntry.isDirectory && nextEntry.name.lowercase(Locale.getDefault()) + .endsWith(".apk") + ) { + entry = nextEntry + mSeenApkFile = true + } + } + + if (entry == null) { + require(mSeenApkFile) { mContext.getString(R.string.installer_error_zip_contains_no_apks) } + + return false + } + + return true + } + + @Throws(Exception::class) + private fun copyAndOpenZip() { + mTempFile = createTempFile() + + mZipFileDescriptor.open().use { `in` -> + FileOutputStream(mTempFile).use { out -> + `in`?.let { + IOUtils.copyStream(`in`, out) + } ?: throw NullPointerException() + } + } + mZipFile = ZipFile(mTempFile) + mZipEntries = mZipFile?.entries() + } + + @Throws(Exception::class) + override fun openApkInputStream(): InputStream? { + return mZipFile?.getInputStream(entry) + } + + override val apkLength: Long + get() = entry?.size?:0 + + override val apkName: String + get() = entry?.let { Utils.getFileNameFromZipEntry(it) }?:"" + + @get:Throws(Exception::class) + override val apkLocalPath: String + get() = entry?.name?:"" + + @Throws(Exception::class) + override fun close() { + mZipFile?.close() + mTempFile?.let { + IOUtils.deleteRecursively(it) + } + } + + override val appName: String? + get() { + return try { + mZipFileDescriptor.name() + } catch (e: Exception) { + null + } + } + + private fun createTempFile(): File { + var tempFile = File(mContext.filesDir, "ZipFileApkSource") + tempFile.mkdir() + tempFile = File(tempFile, System.currentTimeMillis().toString() + ".zip") + return tempFile + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/inter/ApkSource.kt b/app/src/main/java/com/android/grape/sai/inter/ApkSource.kt new file mode 100644 index 0000000..b2866bb --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/inter/ApkSource.kt @@ -0,0 +1,32 @@ +package com.android.grape.sai.inter + +import java.io.InputStream + +interface ApkSource : AutoCloseable { + @Throws(Exception::class) + fun nextApk(): Boolean + + @Throws(Exception::class) + fun openApkInputStream(): InputStream? + + @get:Throws(Exception::class) + val apkLength: Long + + @get:Throws(Exception::class) + val apkName: String? + + @get:Throws(Exception::class) + val apkLocalPath: String? + + @Throws(Exception::class) + override fun close() { + } + + val appName: String? + /** + * Returns the name of the app this ApkSource will install or null if unknown + * + * @return name of the app this ApkSource will install or null if unknown + */ + get() = null +} diff --git a/app/src/main/java/com/android/grape/sai/inter/SaiPackageInstaller.kt b/app/src/main/java/com/android/grape/sai/inter/SaiPackageInstaller.kt new file mode 100644 index 0000000..80162be --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/inter/SaiPackageInstaller.kt @@ -0,0 +1,17 @@ +package com.android.grape.sai.inter + +import com.android.grape.sai.param.SaiPiSessionParams +import com.android.grape.sai.param.SaiPiSessionState + + +interface SaiPackageInstaller { + fun createSession(params: SaiPiSessionParams): String? + + fun enqueueSession(sessionId: String) + + fun registerSessionObserver(observer: SaiPiSessionObserver) + + fun unregisterSessionObserver(observer: SaiPiSessionObserver) + + fun getSessions():List +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/inter/SaiPiSessionObserver.kt b/app/src/main/java/com/android/grape/sai/inter/SaiPiSessionObserver.kt new file mode 100644 index 0000000..734320b --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/inter/SaiPiSessionObserver.kt @@ -0,0 +1,7 @@ +package com.android.grape.sai.inter + +import com.android.grape.sai.param.SaiPiSessionState + +interface SaiPiSessionObserver { + fun onSessionStateChanged(state: SaiPiSessionState?) +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/param/PackageMeta.kt b/app/src/main/java/com/android/grape/sai/param/PackageMeta.kt new file mode 100644 index 0000000..1c638fb --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/param/PackageMeta.kt @@ -0,0 +1,154 @@ +package com.android.grape.sai.param + +import android.content.ContentResolver +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Parcel +import android.os.Parcelable +import com.android.grape.sai.Utils + + +class PackageMeta : Parcelable { + var packageName: String? + var label: String? + var hasSplits: Boolean = false + var isSystemApp: Boolean = false + var versionCode: Long = 0 + var versionName: String? = null + var iconUri: Uri? = null + var installTime: Long = 0 + var updateTime: Long = 0 + + constructor(packageName: String?, label: String?) { + this.packageName = packageName + this.label = label + } + + private constructor(`in`: Parcel) { + packageName = `in`.readString() + label = `in`.readString() + hasSplits = `in`.readInt() == 1 + isSystemApp = `in`.readInt() == 1 + versionCode = `in`.readLong() + versionName = `in`.readString() + iconUri = `in`.readParcelable(Uri::class.java.classLoader) + installTime = `in`.readLong() + updateTime = `in`.readLong() + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(packageName) + dest.writeString(label) + dest.writeInt(if (hasSplits) 1 else 0) + dest.writeInt(if (isSystemApp) 1 else 0) + dest.writeLong(versionCode) + dest.writeString(versionName) + dest.writeParcelable(iconUri, 0) + dest.writeLong(installTime) + dest.writeLong(updateTime) + } + + class Builder(packageName: String?) { + private val mPackageMeta = PackageMeta(packageName, "?") + + fun setLabel(label: String?): Builder { + mPackageMeta.label = label + return this + } + + fun setHasSplits(hasSplits: Boolean): Builder { + mPackageMeta.hasSplits = hasSplits + return this + } + + fun setIsSystemApp(isSystemApp: Boolean): Builder { + mPackageMeta.isSystemApp = isSystemApp + return this + } + + fun setVersionCode(versionCode: Long): Builder { + mPackageMeta.versionCode = versionCode + return this + } + + fun setVersionName(versionName: String?): Builder { + mPackageMeta.versionName = versionName + return this + } + + fun setIcon(iconResId: Int): Builder { + if (iconResId == 0) { + mPackageMeta.iconUri = null + return this + } + + mPackageMeta.iconUri = Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(mPackageMeta.packageName) + .path(iconResId.toString()) + .build() + + return this + } + + fun setIconUri(iconUri: Uri?): Builder { + mPackageMeta.iconUri = iconUri + return this + } + + + fun setInstallTime(installTime: Long): Builder { + mPackageMeta.installTime = installTime + return this + } + + fun setUpdateTime(updateTime: Long): Builder { + mPackageMeta.updateTime = updateTime + return this + } + + fun build(): PackageMeta { + return mPackageMeta + } + } + + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): PackageMeta { + return PackageMeta(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + + fun forPackage(context: Context, packageName: String): PackageMeta? { + try { + val pm = context.packageManager + + val applicationInfo = pm.getApplicationInfo(packageName, 0) + val packageInfo = pm.getPackageInfo(packageName, 0) + + return Builder(applicationInfo.packageName) + .setLabel(applicationInfo.loadLabel(pm).toString()) + .setHasSplits(applicationInfo.splitPublicSourceDirs != null && applicationInfo.splitPublicSourceDirs!!.size > 0) + .setIsSystemApp((applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0) + .setVersionCode(if (Utils.apiIsAtLeast(Build.VERSION_CODES.P)) packageInfo.longVersionCode else packageInfo.versionCode.toLong()) + .setVersionName(packageInfo.versionName) + .setIcon(applicationInfo.icon) + .setInstallTime(packageInfo.firstInstallTime) + .setUpdateTime(packageInfo.lastUpdateTime) + .build() + } catch (e: PackageManager.NameNotFoundException) { + return null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/param/SaiPiSessionParams.kt b/app/src/main/java/com/android/grape/sai/param/SaiPiSessionParams.kt new file mode 100644 index 0000000..01af8e1 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/param/SaiPiSessionParams.kt @@ -0,0 +1,12 @@ +package com.android.grape.sai.param + +import com.android.grape.sai.inter.ApkSource + + +class SaiPiSessionParams(apkSource: ApkSource) { + private val mApkSource: ApkSource = apkSource + + fun apkSource(): ApkSource { + return mApkSource + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/param/SaiPiSessionState.kt b/app/src/main/java/com/android/grape/sai/param/SaiPiSessionState.kt new file mode 100644 index 0000000..a4113d7 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/param/SaiPiSessionState.kt @@ -0,0 +1,143 @@ +package com.android.grape.sai.param + +import android.content.Context +import android.util.Log +import com.android.grape.sai.Stopwatch + + +class SaiPiSessionState private constructor( + private val mSessionId: String, + status: SaiPiSessionStatus +) : + Comparable { + private val mStatus: SaiPiSessionStatus = status + private var mPackageName: String? = null + private var mAppTempName: String? = null + private var mPackageMeta: PackageMeta? = null + private var mLastUpdate: Long + private var mShortError: String? = null + private var mFullError: String? = null + + init { + mLastUpdate = System.currentTimeMillis() + } + + fun sessionId(): String { + return mSessionId + } + + fun status(): SaiPiSessionStatus { + return mStatus + } + + fun packageName(): String? { + return mPackageName + } + + fun appTempName(): String? { + if (mAppTempName != null) return mAppTempName + + if (mPackageName != null) return mPackageName + + return null + } + + fun packageMeta(): PackageMeta? { + return mPackageMeta + } + + /** + * @return user-readable error description + */ + fun shortError(): String? { + return mShortError + } + + /** + * @return full error info for debugging and stuff. May be same as [.shortError] if there's no better info + */ + fun fullError(): String? { + return mFullError + } + + fun lastUpdate(): Long { + return mLastUpdate + } + + fun newBuilder(): Builder { + return Builder(mSessionId, mStatus) + .packageName(packageName()) + .appTempName(appTempName()) + .packageMeta(packageMeta()) + .error(shortError(), fullError()) + } + + override fun hashCode(): Int { + return sessionId().hashCode() + } + + override fun equals(obj: Any?): Boolean { + return obj is SaiPiSessionState && obj.sessionId() == sessionId() + } + + override fun toString(): String { + val sb = StringBuilder() + sb.append( + String.format( + "SaiPiSessionState: sessionId=%s, status=%s", + sessionId(), + status() + ) + ) + return sb.toString() + } + + override fun compareTo(o: SaiPiSessionState): Int { + return java.lang.Long.compare(o.lastUpdate(), lastUpdate()) + } + + class Builder(sessionId: String, status: SaiPiSessionStatus) { + private val mState = SaiPiSessionState(sessionId, status) + + fun packageName(packageName: String?): Builder { + mState.mPackageName = packageName + return this + } + + fun appTempName(tempAppName: String?): Builder { + mState.mAppTempName = tempAppName + return this + } + + fun resolvePackageMeta(c: Context): Builder { + if (mState.mPackageName == null) return this + val packageName = mState.mPackageName + val sw = Stopwatch() + mState.mPackageMeta = PackageMeta.forPackage(c, packageName?:"") + Log.d( + "SaiPiSessionState", + java.lang.String.format("Got PackageMeta in %d ms.", sw.millisSinceStart()) + ) + return this + } + + fun packageMeta(packageMeta: PackageMeta?): Builder { + mState.mPackageMeta = packageMeta + return this + } + + fun error(shortError: String?, fullError: String?): Builder { + var fullError = fullError + mState.mShortError = shortError + if (fullError == null) fullError = shortError + + mState.mFullError = fullError + return this + } + + fun build(): SaiPiSessionState { + mState.mLastUpdate = System.currentTimeMillis() + return mState + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/param/SaiPiSessionStatus.kt b/app/src/main/java/com/android/grape/sai/param/SaiPiSessionStatus.kt new file mode 100644 index 0000000..7cf649b --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/param/SaiPiSessionStatus.kt @@ -0,0 +1,18 @@ +package com.android.grape.sai.param + + +enum class SaiPiSessionStatus { + CREATED, QUEUED, INSTALLING, INSTALLATION_SUCCEED, INSTALLATION_FAILED; + +// fun getReadableName(c: Context): String { +// return when (this) { +// CREATED -> c.getString(R.string.installer_state_created) +// QUEUED -> c.getString(R.string.installer_state_queued) +// INSTALLING -> c.getString(R.string.installer_state_installing) +// INSTALLATION_SUCCEED -> c.getString(R.string.installer_state_installed) +// INSTALLATION_FAILED -> c.getString(R.string.installer_state_failed) +// } +// +// throw IllegalStateException("wtf") +// } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/prefers/DbgPreferencesHelper.kt b/app/src/main/java/com/android/grape/sai/prefers/DbgPreferencesHelper.kt new file mode 100644 index 0000000..549972b --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/prefers/DbgPreferencesHelper.kt @@ -0,0 +1,32 @@ +package com.android.grape.sai.prefers + +import android.content.Context +import android.content.SharedPreferences +import com.android.grape.MainApplication + + +object DbgPreferencesHelper { + const val DONT_REPLACE_DOTS: String = "dbg_dont_replace_dots" + const val CUSTOM_INSTALL_CREATE: String = "dbg_custom_install_create" + const val ADD_FAKE_TIMESTAMP_TO_BACKUPS: String = "dbg_fake_backup_timestamp" + private val mPrefs: SharedPreferences = + MainApplication.instance.getSharedPreferences("com.android.grape.sai.prefers", Context.MODE_PRIVATE) + + + fun shouldReplaceDots(): Boolean { + return !mPrefs.getBoolean(DONT_REPLACE_DOTS, false) + } + + val customInstallCreateCommand: String? + get() { + val command = + mPrefs.getString(CUSTOM_INSTALL_CREATE, "null")!! + if ("null".equals(command, ignoreCase = true)) return null + + return command + } + + fun addFakeTimestampToBackups(): Boolean { + return mPrefs.getBoolean(ADD_FAKE_TIMESTAMP_TO_BACKUPS, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/prefers/PreferencesHelper.kt b/app/src/main/java/com/android/grape/sai/prefers/PreferencesHelper.kt new file mode 100644 index 0000000..7345354 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/prefers/PreferencesHelper.kt @@ -0,0 +1,145 @@ +package com.android.grape.sai.prefers + +import android.content.Context +import android.content.SharedPreferences +import android.os.Environment + + +class PreferencesHelper private constructor(c: Context) { + val prefs: SharedPreferences = c.getSharedPreferences( + "com.android.grape.sai.prefers", + Context.MODE_PRIVATE + ) + + init { + sInstance = this + } + + var homeDirectory: String? + get() = prefs.getString( + PreferencesKeys.HOME_DIRECTORY, + Environment.getExternalStorageDirectory().absolutePath + ) + set(homeDirectory) { + prefs.edit().putString(PreferencesKeys.HOME_DIRECTORY, homeDirectory).apply() + } + + var filePickerRawSort: Int + get() = prefs.getInt(PreferencesKeys.FILE_PICKER_SORT_RAW, 0) + set(rawSort) { + prefs.edit().putInt(PreferencesKeys.FILE_PICKER_SORT_RAW, rawSort).apply() + } + +// var filePickerSortBy: Int +// get() = prefs.getInt(PreferencesKeys.FILE_PICKER_SORT_BY, DialogConfigs.SORT_BY_NAME) +// set(sortBy) { +// prefs.edit().putInt(PreferencesKeys.FILE_PICKER_SORT_BY, sortBy).apply() +// } +// +// var filePickerSortOrder: Int +// get() = prefs.getInt( +// PreferencesKeys.FILE_PICKER_SORT_ORDER, +// DialogConfigs.SORT_ORDER_NORMAL +// ) +// set(sortOrder) { +// prefs.edit().putInt(PreferencesKeys.FILE_PICKER_SORT_ORDER, sortOrder).apply() +// } + + fun shouldSignApks(): Boolean { + return prefs.getBoolean(PreferencesKeys.SIGN_APKS, false) + } + + fun setShouldSignApks(signApks: Boolean) { + prefs.edit().putBoolean(PreferencesKeys.SIGN_APKS, signApks).apply() + } + + fun shouldExtractArchives(): Boolean { + return prefs.getBoolean(PreferencesKeys.EXTRACT_ARCHIVES, false) + } + + fun shouldUseZipFileApi(): Boolean { + return prefs.getBoolean(PreferencesKeys.USE_ZIPFILE, false) + } + + var installer: Int + get() = prefs.getInt(PreferencesKeys.INSTALLER, PreferencesValues.INSTALLER_ROOTED) + set(installer) { + prefs.edit().putInt(PreferencesKeys.INSTALLER, installer).apply() + } + + var backupFileNameFormat: String? + get() = prefs.getString( + PreferencesKeys.BACKUP_FILE_NAME_FORMAT, + PreferencesValues.BACKUP_FILE_NAME_FORMAT_DEFAULT + ) + set(format) { + prefs.edit().putString(PreferencesKeys.BACKUP_FILE_NAME_FORMAT, format).apply() + } + + var installLocation: Int + get() { + val rawInstallLocation = + prefs.getString(PreferencesKeys.INSTALL_LOCATION, "0")!! + return try { + rawInstallLocation.toInt() + } catch (e: NumberFormatException) { + 0 + } + } + set(installLocation) { + prefs.edit().putString(PreferencesKeys.INSTALL_LOCATION, installLocation.toString()) + .apply() + } + + fun useOldInstaller(): Boolean { + return prefs.getBoolean(PreferencesKeys.USE_OLD_INSTALLER, false) + } + + fun showInstallerDialogs(): Boolean { + return prefs.getBoolean(PreferencesKeys.SHOW_INSTALLER_DIALOGS, true) + } + + fun shouldShowAppFeatures(): Boolean { + return prefs.getBoolean(PreferencesKeys.SHOW_APP_FEATURES, true) + } + + fun shouldShowSafTip(): Boolean { + return !prefs.getBoolean(PreferencesKeys.SAF_TIP_SHOWN, false) + } + + fun setSafTipShown() { + prefs.edit().putBoolean(PreferencesKeys.SAF_TIP_SHOWN, true).apply() + } + + val isInstallerXEnabled: Boolean + get() = prefs.getBoolean(PreferencesKeys.USE_INSTALLERX, true) + + val isBruteParserEnabled: Boolean + get() = prefs.getBoolean(PreferencesKeys.USE_BRUTE_PARSER, true) + + var isAnalyticsEnabled: Boolean + get() = prefs.getBoolean(PreferencesKeys.ENABLE_ANALYTICS, true) + set(enabled) { + prefs.edit().putBoolean(PreferencesKeys.ENABLE_ANALYTICS, enabled).apply() + } + + var isInitialIndexingDone: Boolean + get() = prefs.getBoolean(PreferencesKeys.INITIAL_INDEXING_RUN, false) + set(done) { + prefs.edit().putBoolean(PreferencesKeys.INITIAL_INDEXING_RUN, done).apply() + } + + var isSingleApkExportEnabled: Boolean + get() = prefs.getBoolean(PreferencesKeys.BACKUP_APK_EXPORT, false) + set(enabled) { + prefs.edit().putBoolean(PreferencesKeys.BACKUP_APK_EXPORT, enabled).apply() + } + + companion object { + private lateinit var sInstance: PreferencesHelper + + fun getInstance(c: Context): PreferencesHelper { + return sInstance ?: PreferencesHelper(c) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/prefers/PreferencesKeys.kt b/app/src/main/java/com/android/grape/sai/prefers/PreferencesKeys.kt new file mode 100644 index 0000000..445aebb --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/prefers/PreferencesKeys.kt @@ -0,0 +1,31 @@ +package com.android.grape.sai.prefers + +object PreferencesKeys { + const val CURRENT_THEME: String = "current_theme" + const val THEME_MODE: String = "theme_mode" + const val HOME_DIRECTORY: String = "home_directory" + const val FILE_PICKER_SORT_RAW: String = "file_picker_sort_raw" + const val FILE_PICKER_SORT_BY: String = "file_picker_sort_by" + const val FILE_PICKER_SORT_ORDER: String = "file_picker_sort_order" + const val SIGN_APKS: String = "sign_apks" + const val MIUI_WARNING_SHOWN: String = "miui_warning_shown" + const val EXTRACT_ARCHIVES: String = "extract_archives" + const val USE_ZIPFILE: String = "use_zipfile" + const val INSTALLER: String = "installer" + const val BACKUP_FILE_NAME_FORMAT: String = "backup_file_name_format" + const val INSTALL_LOCATION: String = "install_location" + const val USE_OLD_INSTALLER: String = "use_old_installer" + const val SHOW_INSTALLER_DIALOGS: String = "show_installer_dialogs" + const val SHOW_APP_FEATURES: String = "show_app_features" + const val THEME: String = "theme" + const val SAF_TIP_SHOWN: String = "saf_tip_shown" + const val AUTO_THEME: String = "auto_theme" + const val AUTO_THEME_PICKER: String = "auto_theme_picker" + const val USE_INSTALLERX: String = "use_installerx" + const val USE_BRUTE_PARSER: String = "use_brute_parser" + const val ENABLE_ANALYTICS: String = "firebase_enabled" + const val INITIAL_INDEXING_RUN: String = "initial_indexing_run" + const val BACKUP_SETTINGS: String = "backup_settings" + const val BACKUP_APK_EXPORT: String = "single_apk_export" + const val ENABLE_APK_ACTION_VIEW: String = "enable_apk_action_view" +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/prefers/PreferencesValues.kt b/app/src/main/java/com/android/grape/sai/prefers/PreferencesValues.kt new file mode 100644 index 0000000..e5f2d68 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/prefers/PreferencesValues.kt @@ -0,0 +1,9 @@ +package com.android.grape.sai.prefers + +object PreferencesValues { + const val INSTALLER_ROOTLESS: Int = 0 + const val INSTALLER_ROOTED: Int = 1 + const val INSTALLER_SHIZUKU: Int = 2 + + const val BACKUP_FILE_NAME_FORMAT_DEFAULT: String = "NAME_PACKAGE_VERSION" +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/rootless/AndroidPackageInstallerError.kt b/app/src/main/java/com/android/grape/sai/rootless/AndroidPackageInstallerError.kt new file mode 100644 index 0000000..f7c0d11 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/rootless/AndroidPackageInstallerError.kt @@ -0,0 +1,270 @@ +package com.android.grape.sai.rootless + +import android.content.Context +import androidx.annotation.StringRes +import com.android.grape.R + +enum class AndroidPackageInstallerError( + /** + * @return error name (maybe more like a string code lol) + */ + val error: String, + /** + * @return "legacy" error code used in Android\'s PackageManager + */ + val legacyErrorCode: Int, @field:StringRes @param:StringRes private val mDescription: Int +) { + UNKNOWN("UNKNOWN", 1337, R.string.installer_rootless_error2_unknown), + INSTALL_FAILED_ALREADY_EXISTS( + "INSTALL_FAILED_ALREADY_EXISTS", + -1, + R.string.installer_rootless_error2_install_failed_already_exists + ), + INSTALL_FAILED_INVALID_APK( + "INSTALL_FAILED_INVALID_APK", + -2, + R.string.installer_rootless_error2_install_failed_invalid_apk + ), + INSTALL_FAILED_INVALID_URI( + "INSTALL_FAILED_INVALID_URI", + -3, + R.string.installer_rootless_error2_install_failed_invalid_uri + ), + INSTALL_FAILED_INSUFFICIENT_STORAGE( + "INSTALL_FAILED_INSUFFICIENT_STORAGE", + -4, + R.string.installer_rootless_error2_install_failed_insufficient_storage + ), + INSTALL_FAILED_DUPLICATE_PACKAGE( + "INSTALL_FAILED_DUPLICATE_PACKAGE", + -5, + R.string.installer_rootless_error2_install_failed_duplicate_package + ), + INSTALL_FAILED_NO_SHARED_USER( + "INSTALL_FAILED_NO_SHARED_USER", + -6, + R.string.installer_rootless_error2_install_failed_no_shared_user + ), + INSTALL_FAILED_UPDATE_INCOMPATIBLE( + "INSTALL_FAILED_UPDATE_INCOMPATIBLE", + -7, + R.string.installer_rootless_error2_install_failed_update_incompatible + ), + INSTALL_FAILED_SHARED_USER_INCOMPATIBLE( + "INSTALL_FAILED_SHARED_USER_INCOMPATIBLE", + -8, + R.string.installer_rootless_error2_install_failed_shared_user_incompatible + ), + INSTALL_FAILED_MISSING_SHARED_LIBRARY( + "INSTALL_FAILED_MISSING_SHARED_LIBRARY", + -9, + R.string.installer_rootless_error2_install_failed_missing_shared_library + ), + INSTALL_FAILED_REPLACE_COULDNT_DELETE( + "INSTALL_FAILED_REPLACE_COULDNT_DELETE", + -10, + R.string.installer_rootless_error2_install_failed_replace_couldnt_delete + ), + INSTALL_FAILED_DEXOPT( + "INSTALL_FAILED_DEXOPT", + -11, + R.string.installer_rootless_error2_install_failed_dexopt + ), + INSTALL_FAILED_OLDER_SDK( + "INSTALL_FAILED_OLDER_SDK", + -12, + R.string.installer_rootless_error2_install_failed_older_sdk + ), + INSTALL_FAILED_CONFLICTING_PROVIDER( + "INSTALL_FAILED_CONFLICTING_PROVIDER", + -13, + R.string.installer_rootless_error2_install_failed_conflicting_provider + ), + INSTALL_FAILED_NEWER_SDK( + "INSTALL_FAILED_NEWER_SDK", + -14, + R.string.installer_rootless_error2_install_failed_newer_sdk + ), + INSTALL_FAILED_TEST_ONLY( + "INSTALL_FAILED_TEST_ONLY", + -15, + R.string.installer_rootless_error2_install_failed_test_only + ), + INSTALL_FAILED_CPU_ABI_INCOMPATIBLE( + "INSTALL_FAILED_CPU_ABI_INCOMPATIBLE", + -16, + R.string.installer_rootless_error2_install_failed_cpu_abi_incompatible + ), + INSTALL_FAILED_MISSING_FEATURE( + "INSTALL_FAILED_MISSING_FEATURE", + -17, + R.string.installer_rootless_error2_install_failed_missing_feature + ), + INSTALL_FAILED_CONTAINER_ERROR( + "INSTALL_FAILED_CONTAINER_ERROR", + -18, + R.string.installer_rootless_error2_install_failed_container_error + ), + INSTALL_FAILED_INVALID_INSTALL_LOCATION( + "INSTALL_FAILED_INVALID_INSTALL_LOCATION", + -19, + R.string.installer_rootless_error2_install_failed_invalid_install_location + ), + INSTALL_FAILED_MEDIA_UNAVAILABLE( + "INSTALL_FAILED_MEDIA_UNAVAILABLE", + -20, + R.string.installer_rootless_error2_install_failed_media_unavailable + ), + INSTALL_FAILED_VERIFICATION_TIMEOUT( + "INSTALL_FAILED_VERIFICATION_TIMEOUT", + -21, + R.string.installer_rootless_error2_install_failed_verification_timeout + ), + INSTALL_FAILED_VERIFICATION_FAILURE( + "INSTALL_FAILED_VERIFICATION_FAILURE", + -22, + R.string.installer_rootless_error2_install_failed_verification_failure + ), + INSTALL_FAILED_PACKAGE_CHANGED( + "INSTALL_FAILED_PACKAGE_CHANGED", + -23, + R.string.installer_rootless_error2_install_failed_package_changed + ), + INSTALL_FAILED_UID_CHANGED( + "INSTALL_FAILED_UID_CHANGED", + -24, + R.string.installer_rootless_error2_install_failed_uid_changed + ), + INSTALL_FAILED_VERSION_DOWNGRADE( + "INSTALL_FAILED_VERSION_DOWNGRADE", + -25, + R.string.installer_rootless_error2_install_failed_version_downgrade + ), + INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE( + "INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE", + -26, + R.string.installer_rootless_error2_install_failed_permission_model_downgrade + ), + INSTALL_FAILED_SANDBOX_VERSION_DOWNGRADE( + "INSTALL_FAILED_SANDBOX_VERSION_DOWNGRADE", + -27, + R.string.installer_rootless_error2_install_failed_sandbox_version_downgrade + ), + INSTALL_FAILED_MISSING_SPLIT( + "INSTALL_FAILED_MISSING_SPLIT", + -28, + R.string.installer_rootless_error2_install_failed_missing_split + ), + INSTALL_PARSE_FAILED_NOT_APK( + "INSTALL_PARSE_FAILED_NOT_APK", + -100, + R.string.installer_rootless_error2_install_parse_failed_not_apk + ), + INSTALL_PARSE_FAILED_BAD_MANIFEST( + "INSTALL_PARSE_FAILED_BAD_MANIFEST", + -101, + R.string.installer_rootless_error2_install_parse_failed_bad_manifest + ), + INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION( + "INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION", + -102, + R.string.installer_rootless_error2_install_parse_failed_unexpected_exception + ), + INSTALL_PARSE_FAILED_NO_CERTIFICATES( + "INSTALL_PARSE_FAILED_NO_CERTIFICATES", + -103, + R.string.installer_rootless_error2_install_parse_failed_no_certificates + ), + INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES( + "INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES", + -104, + R.string.installer_rootless_error2_install_parse_failed_inconsistent_certificates + ), + INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING( + "INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING", + -105, + R.string.installer_rootless_error2_install_parse_failed_certificate_encoding + ), + INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME( + "INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME", + -106, + R.string.installer_rootless_error2_install_parse_failed_bad_package_name + ), + INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID( + "INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID", + -107, + R.string.installer_rootless_error2_install_parse_failed_bad_shared_user_id + ), + INSTALL_PARSE_FAILED_MANIFEST_MALFORMED( + "INSTALL_PARSE_FAILED_MANIFEST_MALFORMED", + -108, + R.string.installer_rootless_error2_install_parse_failed_manifest_malformed + ), + INSTALL_PARSE_FAILED_MANIFEST_EMPTY( + "INSTALL_PARSE_FAILED_MANIFEST_EMPTY", + -109, + R.string.installer_rootless_error2_install_parse_failed_manifest_empty + ), + INSTALL_FAILED_INTERNAL_ERROR( + "INSTALL_FAILED_INTERNAL_ERROR", + -110, + R.string.installer_rootless_error2_install_failed_internal_error + ), + INSTALL_FAILED_USER_RESTRICTED( + "INSTALL_FAILED_USER_RESTRICTED", + -111, + R.string.installer_rootless_error2_install_failed_user_restricted + ), + INSTALL_FAILED_DUPLICATE_PERMISSION( + "INSTALL_FAILED_DUPLICATE_PERMISSION", + -112, + R.string.installer_rootless_error2_install_failed_duplicate_permission + ), + INSTALL_FAILED_NO_MATCHING_ABIS( + "INSTALL_FAILED_NO_MATCHING_ABIS", + -113, + R.string.installer_rootless_error2_install_failed_no_matching_abis + ), + INSTALL_FAILED_ABORTED( + "INSTALL_FAILED_ABORTED", + -115, + R.string.installer_rootless_error2_install_failed_aborted + ), + INSTALL_FAILED_INSTANT_APP_INVALID( + "INSTALL_FAILED_INSTANT_APP_INVALID", + -116, + R.string.installer_rootless_error2_install_failed_instant_app_invalid + ), + INSTALL_FAILED_BAD_DEX_METADATA( + "INSTALL_FAILED_BAD_DEX_METADATA", + -117, + R.string.installer_rootless_error2_install_failed_bad_dex_metadata + ), + INSTALL_FAILED_BAD_SIGNATURE( + "INSTALL_FAILED_BAD_SIGNATURE", + -118, + R.string.installer_rootless_error2_install_failed_bad_signature + ), + INSTALL_FAILED_OTHER_STAGED_SESSION_IN_PROGRESS( + "INSTALL_FAILED_OTHER_STAGED_SESSION_IN_PROGRESS", + -119, + R.string.installer_rootless_error2_install_failed_other_staged_session_in_progress + ), + INSTALL_FAILED_MULTIPACKAGE_INCONSISTENCY( + "INSTALL_FAILED_MULTIPACKAGE_INCONSISTENCY", + -120, + R.string.installer_rootless_error2_install_failed_multipackage_inconsistency + ), + INSTALL_FAILED_WRONG_INSTALLED_VERSION( + "INSTALL_FAILED_WRONG_INSTALLED_VERSION", + -121, + R.string.installer_rootless_error2_install_failed_wrong_installed_version + ); + + /** + * @return human readable error description + */ + fun getDescription(context: Context): String { + return context.getString(mDescription) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/rootless/ConfirmationIntentWrapperActivity2.kt b/app/src/main/java/com/android/grape/sai/rootless/ConfirmationIntentWrapperActivity2.kt new file mode 100644 index 0000000..5c0c8fd --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/rootless/ConfirmationIntentWrapperActivity2.kt @@ -0,0 +1,78 @@ +package com.android.grape.sai.rootless + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInstaller +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.android.grape.sai.RootlessSaiPiBroadcastReceiver + + +class ConfirmationIntentWrapperActivity2 : AppCompatActivity() { + private var mFinishedProperly = false + + private var mSessionId = 0 + private var mConfirmationIntent: Intent? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val intent = intent + + mSessionId = intent.getIntExtra(EXTRA_SESSION_ID, -1) + mConfirmationIntent = intent.getParcelableExtra(EXTRA_CONFIRMATION_INTENT) + + if (savedInstanceState == null) { + try { + startActivityForResult(mConfirmationIntent!!, REQUEST_CODE_CONFIRM_INSTALLATION) + } catch (e: Exception) { + sendErrorBroadcast(mSessionId, RootlessSaiPiBroadcastReceiver.STATUS_BAD_ROM) + finish() + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == REQUEST_CODE_CONFIRM_INSTALLATION) { + mFinishedProperly = true + finish() + } + } + + override fun onDestroy() { + super.onDestroy() + + if (isFinishing && !mFinishedProperly) start( + this, mSessionId, mConfirmationIntent + ) //Because if user doesn't confirm/cancel the installation, PackageInstaller session will hang + } + + private fun sendErrorBroadcast(sessionID: Int, status: Int) { + val statusIntent: Intent = Intent(RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT) + statusIntent.putExtra(PackageInstaller.EXTRA_STATUS, status) + statusIntent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionID) + + sendBroadcast(statusIntent) + } + + companion object { + private const val EXTRA_CONFIRMATION_INTENT = "confirmation_intent" + const val EXTRA_SESSION_ID: String = "session_id" + + private const val REQUEST_CODE_CONFIRM_INSTALLATION = 322 + + fun start(c: Context, sessionId: Int, confirmationIntent: Intent?) { + val intent = Intent( + c, + ConfirmationIntentWrapperActivity2::class.java + ) + intent.putExtra(EXTRA_CONFIRMATION_INTENT, confirmationIntent) + intent.putExtra(EXTRA_SESSION_ID, sessionId) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + c.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/rootless/RootlessSaiPackageInstaller.kt b/app/src/main/java/com/android/grape/sai/rootless/RootlessSaiPackageInstaller.kt new file mode 100644 index 0000000..7cbbe99 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/rootless/RootlessSaiPackageInstaller.kt @@ -0,0 +1,174 @@ +package com.android.grape.sai.rootless + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.util.Log +import com.android.grape.sai.BaseSaiPackageInstaller +import com.android.grape.sai.IOUtils +import com.android.grape.sai.RootlessSaiPiBroadcastReceiver +import com.android.grape.sai.Utils +import com.android.grape.sai.param.SaiPiSessionParams +import com.android.grape.sai.param.SaiPiSessionState +import com.android.grape.sai.param.SaiPiSessionStatus +import com.android.grape.sai.prefers.PreferencesHelper +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +@SuppressLint("UnspecifiedRegisterReceiverFlag") +class RootlessSaiPackageInstaller private constructor(c: Context) : + BaseSaiPackageInstaller(c), RootlessSaiPiBroadcastReceiver.EventObserver { + private val mPackageInstaller: PackageInstaller = + context.packageManager.packageInstaller + private val mExecutor: ExecutorService = Executors.newFixedThreadPool(4) + private val mWorkerThread = HandlerThread("RootlessSaiPi Worker") + private val mWorkerHandler: Handler + + private val mAndroidPiSessionIdToSaiPiSessionId = ConcurrentHashMap() + private val mSessionIdToAppTempName = ConcurrentHashMap() + + private val mBroadcastReceiver: RootlessSaiPiBroadcastReceiver + + init { + mWorkerThread.start() + mWorkerHandler = Handler(mWorkerThread.looper) + + mBroadcastReceiver = RootlessSaiPiBroadcastReceiver(context) + mBroadcastReceiver.addEventObserver(this) + context.registerReceiver( + mBroadcastReceiver, + IntentFilter(RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT), + "", + mWorkerHandler + ) + + sInstance = this + } + + override fun enqueueSession(sessionId: String) { + val params: SaiPiSessionParams? = takeCreatedSession(sessionId)?.apply { + setSessionState( + sessionId, + SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.QUEUED).appTempName( + apkSource().appName + ).build() + ) + mExecutor.submit { install(sessionId, this) } + } + } + + private fun install(sessionId: String, params: SaiPiSessionParams) { + var session: PackageInstaller.Session? = null + var appTempName: String? = null + try { + params.apkSource().use { apkSource -> + appTempName = apkSource.appName + if (appTempName != null) mSessionIdToAppTempName[sessionId] = appTempName!! + + setSessionState( + sessionId, + SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLING).appTempName(appTempName) + .build() + ) + + val sessionParams = + PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) + sessionParams.setInstallLocation( + PreferencesHelper.getInstance(context).installLocation + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) sessionParams.setInstallReason( + PackageManager.INSTALL_REASON_USER + ) + + val androidSessionId = mPackageInstaller.createSession(sessionParams) + mAndroidPiSessionIdToSaiPiSessionId[androidSessionId] = sessionId + + session = mPackageInstaller.openSession(androidSessionId) + var currentApkFile = 0 + while (apkSource.nextApk()) { + apkSource.openApkInputStream().use { inputStream -> + session!!.openWrite( + String.format("%d.apk", currentApkFile++), + 0, + apkSource.apkLength + ).use { outputStream -> + if (inputStream != null) { + IOUtils.copyStream(inputStream, outputStream) + } + session!!.fsync(outputStream) + } + } + } + + val callbackIntent: Intent = + Intent(RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT) + val pendingIntent = PendingIntent.getBroadcast(context, 0, callbackIntent, + 0) + session!!.commit(pendingIntent.intentSender) + } + } catch (e: Exception) { + Log.w(TAG, e) + if (session != null) session!!.abandon() + + setSessionState( + sessionId, + SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName(appTempName) + .error(e.localizedMessage, Utils.throwableToString(e)).build() + ) + } finally { + if (session != null) session!!.close() + } + } + + override fun onInstallationSucceeded(androidSessionId: Int, packageName: String?) { + val sessionId = mAndroidPiSessionIdToSaiPiSessionId[androidSessionId] ?: return + + setSessionState( + sessionId, + SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_SUCCEED).packageName(packageName) + .resolvePackageMeta(context).build() + ) + } + + override fun onInstallationFailed( + androidSessionId: Int, + shortError: String?, + fullError: String?, + exception: Exception? + ) { + val sessionId = mAndroidPiSessionIdToSaiPiSessionId[androidSessionId] ?: return + + setSessionState( + sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED) + .appTempName(mSessionIdToAppTempName.remove(sessionId)) + .error(shortError, fullError) + .build() + ) + } + + protected override fun tag(): String { + return TAG + } + + companion object { + private const val TAG = "RootlessSaiPi" + + @SuppressLint("StaticFieldLeak") + private var sInstance: RootlessSaiPackageInstaller? = null + + fun getInstance(c: Context): RootlessSaiPackageInstaller { + synchronized(RootlessSaiPackageInstaller::class.java) { + return sInstance + ?: RootlessSaiPackageInstaller(c) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/shell/MiuiUtils.kt b/app/src/main/java/com/android/grape/sai/shell/MiuiUtils.kt new file mode 100644 index 0000000..8f07c3e --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/shell/MiuiUtils.kt @@ -0,0 +1,85 @@ +package com.android.grape.sai.shell + +import android.annotation.SuppressLint +import android.os.Build +import android.text.TextUtils +import com.android.grape.sai.Utils +import java.util.Objects + +object MiuiUtils { + val isMiui: Boolean + get() = !TextUtils.isEmpty(Utils.getSystemProperty("ro.miui.ui.version.name")) + + val miuiVersionName: String + get() { + val versionName: String? = Utils.getSystemProperty("ro.miui.ui.version.name") + return versionName?:"???" + } + + val miuiVersionCode: Int + get() { + return try { + Objects.requireNonNull(Utils.getSystemProperty("ro.miui.ui.version.code"))?.toInt()?:0 + } catch (e: Exception) { + -1 + } + } + + val actualMiuiVersion: String + get() = Build.VERSION.INCREMENTAL + + private fun parseVersionIntoParts(version: String): IntArray { + try { + val versionParts = + version.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val intVersionParts = IntArray(versionParts.size) + + for (i in versionParts.indices) intVersionParts[i] = versionParts[i].toInt() + + return intVersionParts + } catch (e: Exception) { + return intArrayOf(-1) + } + } + + /** + * @return 0 if versions are equal, values less than 0 if ver1 is lower than ver2, value more than 0 if ver1 is higher than ver2 + */ + private fun compareVersions(version1: String, version2: String): Int { + if (version1 == version2) return 0 + + val version1Parts = parseVersionIntoParts(version1) + val version2Parts = parseVersionIntoParts(version2) + + for (i in version2Parts.indices) { + if (i >= version1Parts.size) return -1 + + if (version1Parts[i] < version2Parts[i]) return -1 + + if (version1Parts[i] > version2Parts[i]) return 1 + } + + return 1 + } + + fun isActualMiuiVersionAtLeast(targetVer: String): Boolean { + return compareVersions(actualMiuiVersion, targetVer) >= 0 + } + + @get:SuppressLint("PrivateApi") + val isMiuiOptimizationDisabled: Boolean + get() { + if ("0" == Utils.getSystemProperty("persist.sys.miui_optimization")) return true + + return try { + Class.forName("android.miui.AppOpsUtils") + .getDeclaredMethod("isXOptMode") + .invoke(null) as Boolean + } catch (e: Exception) { + false + } + } + + val isFixedMiui: Boolean + get() = isActualMiuiVersionAtLeast("20.2.20") || isMiuiOptimizationDisabled +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/sai/shell/Shell.kt b/app/src/main/java/com/android/grape/sai/shell/Shell.kt new file mode 100644 index 0000000..6e078a6 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/shell/Shell.kt @@ -0,0 +1,78 @@ +package com.android.grape.sai.shell + +import android.annotation.SuppressLint +import java.io.InputStream + + +interface Shell { + fun isAvailable(): Boolean + + fun exec(command: Command): Result + + fun exec(command: Command, inputPipe: InputStream): Result + + fun makeLiteral(arg: String): String + + class Command(command: String, vararg args: String) { + private val mArgs = ArrayList() + + init { + mArgs.add(command) + mArgs.addAll(listOf(*args)) + } + + fun toStringArray(): Array { + val array = arrayOfNulls(mArgs.size) + + for (i in mArgs.indices) array[i] = mArgs[i] + + return array + } + + override fun toString(): String { + val sb = StringBuilder() + + for (i in mArgs.indices) { + val arg = mArgs[i] + sb.append(arg) + if (i < mArgs.size - 1) sb.append(" ") + } + + return sb.toString() + } + + class Builder(command: String, vararg args: String) { + private val mCommand = Command(command, *args) + + fun addArg(argument: String): Builder { + mCommand.mArgs.add(argument) + return this + } + + fun build(): Command { + return mCommand + } + } + } + + class Result ( + var cmd: Command, + var exitCode: Int, + var out: String, + var err: String + ) { + val isSuccessful: Boolean + get() = exitCode == 0 + + @SuppressLint("DefaultLocale") + override fun toString(): String { + return String.format( + "Command: %s\nExit code: %d\nOut:\n%s\n=============\nErr:\n%s", + cmd, + exitCode, + out, + err + ) + } + } +} diff --git a/app/src/main/java/com/android/grape/sai/shell/ShizukuShell.kt b/app/src/main/java/com/android/grape/sai/shell/ShizukuShell.kt new file mode 100644 index 0000000..f6095f2 --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/shell/ShizukuShell.kt @@ -0,0 +1,110 @@ +package com.android.grape.sai.shell + +import android.os.Build +import android.util.Log +import com.android.grape.sai.IOUtils +import com.android.grape.sai.Utils +import rikka.shizuku.Shizuku +import rikka.shizuku.ShizukuRemoteProcess +import java.io.InputStream + + +class ShizukuShell private constructor() : Shell { + init { + sInstance = this + } + + override fun isAvailable(): Boolean { + if (!Shizuku.pingBinder()) return false + + try { + return exec(Shell.Command("echo", "test")).isSuccessful + } catch (e: Exception) { + Log.w(TAG, "Unable to access shizuku: ") + Log.w(TAG, e) + return false + } + } + + override fun exec(command: Shell.Command): Shell.Result { + return execInternal(command, null) + } + + override fun exec(command: Shell.Command, inputPipe: InputStream): Shell.Result { + return execInternal(command, inputPipe) + } + + override fun makeLiteral(arg: String): String { + return "'" + arg.replace("'", "'\\''") + "'" + } + + private fun execInternal(command: Shell.Command, inputPipe: InputStream?): Shell.Result { + val stdOutSb = StringBuilder() + val stdErrSb = StringBuilder() + + try { + val shCommand: Shell.Command.Builder = + Shell.Command.Builder("sh", "-c", command.toString()) + + val process: ShizukuRemoteProcess = + Shizuku.newProcess(shCommand.build().toStringArray(), null, null) + + val stdOutD: Thread = + IOUtils.writeStreamToStringBuilder(stdOutSb, process.getInputStream()) + val stdErrD: Thread = + IOUtils.writeStreamToStringBuilder(stdErrSb, process.getErrorStream()) + + if (inputPipe != null) { + try { + process.getOutputStream().use { outputStream -> + inputPipe.use { inputStream -> + IOUtils.copyStream(inputStream, outputStream) + } + } + } catch (e: Exception) { + stdOutD.interrupt() + stdErrD.interrupt() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) process.destroyForcibly() + else process.destroy() + + throw RuntimeException(e) + } + } + + process.waitFor() + stdOutD.join() + stdErrD.join() + + return Shell.Result( + command, + process.exitValue(), + stdOutSb.toString().trim { it <= ' ' }, + stdErrSb.toString().trim { it <= ' ' }) + } catch (e: Exception) { + Log.w(TAG, "Unable execute command: ") + Log.w(TAG, e) + return Shell.Result( + command, -1, stdOutSb.toString().trim { it <= ' ' }, """ + $stdErrSb + + SAI ShizukuShell Java exception: ${Utils.throwableToString(e)} + """.trimIndent() + ) + } + } + + companion object { + private const val TAG = "ShizukuShell" + + private var sInstance: ShizukuShell? = null + + val instance: ShizukuShell + get() { + synchronized(ShizukuShell::class.java) { + return sInstance + ?: ShizukuShell() + } + } + } +} diff --git a/app/src/main/java/com/android/grape/sai/shell/SuShell.kt b/app/src/main/java/com/android/grape/sai/shell/SuShell.kt new file mode 100644 index 0000000..afe061a --- /dev/null +++ b/app/src/main/java/com/android/grape/sai/shell/SuShell.kt @@ -0,0 +1,73 @@ +package com.android.grape.sai.shell + +import android.util.Log +import com.android.grape.sai.Utils +import com.android.grape.util.MockTools +import java.io.InputStream + +class SuShell private constructor() : Shell { + init { + sInstance = this + } + + fun requestRoot(): Boolean { + try { + return exec(Shell.Command("exit")).isSuccessful + } catch (e: Exception) { + Log.w(TAG, "Unable to acquire root access: ") + Log.w(TAG, e) + return false + } + } + + override fun isAvailable(): Boolean { + return requestRoot() + } + + override fun exec(command: Shell.Command): Shell.Result { + return execInternal(command, null) + } + + override fun exec(command: Shell.Command, inputPipe: InputStream): Shell.Result { + return execInternal(command, inputPipe) + } + + override fun makeLiteral(arg: String): String { + return "'" + arg.replace("'", "'\\''") + "'" + } + + private fun execInternal(command: Shell.Command, inputPipe: InputStream?): Shell.Result { + val stdOutSb = StringBuilder() + val stdErrSb = StringBuilder() + try { + val res: String = MockTools.execRead(command.toString()) + var result_code = -1 + if (res !== "") result_code = 0 + return Shell.Result(command, result_code, res, stdErrSb.toString().trim { it <= ' ' }) + } catch (e: Exception) { + Log.w(TAG, "Unable execute command: ") + Log.w(TAG, e) + return Shell.Result( + command, -1, stdOutSb.toString().trim { it <= ' ' }, """ + $stdErrSb + + SAI SuShell Java exception: ${Utils.throwableToString(e)} + """.trimIndent() + ) + } + } + + companion object { + private const val TAG = "SuShell" + + private var sInstance: SuShell? = null + + val instance: SuShell + get() { + synchronized(SuShell::class.java) { + return sInstance + ?: SuShell() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/service/MyAccessibilityService.kt b/app/src/main/java/com/android/grape/service/MyAccessibilityService.kt new file mode 100644 index 0000000..77dc64a --- /dev/null +++ b/app/src/main/java/com/android/grape/service/MyAccessibilityService.kt @@ -0,0 +1,99 @@ +package com.android.grape.service + +import android.Manifest +import android.accessibilityservice.AccessibilityService +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo +import android.os.Build +import android.view.accessibility.AccessibilityEvent +import android.widget.Toast +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import com.android.grape.MainActivity +import com.android.grape.R + +class MyAccessibilityService:AccessibilityService(){ + + override fun onAccessibilityEvent(event: AccessibilityEvent?) { + } + + override fun onInterrupt() { + } + + override fun onServiceConnected() { + super.onServiceConnected() + startForegroundService() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + "2", + "无障碍服务", + NotificationManager.IMPORTANCE_LOW + ) + channel.description = "此服务在后台运行,提供无障碍支持。" + val manager: NotificationManager = getSystemService( + NotificationManager::class.java + ) + if (manager != null) { + manager.createNotificationChannel(channel) + } + } + } + + private fun startForegroundService() { + createNotificationChannel() + + val notificationIntent: Intent = Intent( + this, + MainActivity::class.java + ) + val pendingIntentFlags = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 + val pendingIntent = + PendingIntent.getActivity(this, 0, notificationIntent, pendingIntentFlags) + + val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(this, "2") + .setContentTitle("无障碍服务运行中") + .setContentText("此服务正在持续运行") + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentIntent(pendingIntent) + + val notification = notificationBuilder.build() + + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION + ) + != PackageManager.PERMISSION_GRANTED + ) { + Toast.makeText(this, "请授予前台服务权限以启动服务", Toast.LENGTH_SHORT).show() + return + } + + val requiredPermissions = arrayOf( + Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION, + "android.permission.CAPTURE_VIDEO_OUTPUT" + ) + + for (permission in requiredPermissions) { + if (ContextCompat.checkSelfPermission( + this, + permission + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION) + } else { + startForeground(1, notification) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/AdvertisingIdClient.kt b/app/src/main/java/com/android/grape/util/AdvertisingIdClient.kt new file mode 100644 index 0000000..21cf25c --- /dev/null +++ b/app/src/main/java/com/android/grape/util/AdvertisingIdClient.kt @@ -0,0 +1,119 @@ +package com.android.grape.util + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.os.IInterface +import android.os.Looper +import android.os.Parcel +import android.os.RemoteException +import android.util.Log +import java.util.concurrent.LinkedBlockingQueue + + +/** + * date:2020-11-13 + * desc: + */ +object AdvertisingIdClient { + private const val TAG = "AdvertisingIdClient" + + private fun logd(msg: String) { + Log.d(TAG, msg) + } + + /** + * 这个方法是耗时的,不能在主线程调用 + */ + @Throws(Exception::class) + fun getGoogleAdId(context: Context): String? { + if (Looper.getMainLooper() == Looper.myLooper()) { + logd("getGoogleAdId not running in main thread#########") + return null + } + val pm = context.packageManager + pm.getPackageInfo("com.android.vending", 0) + val connection = AdvertisingConnection() + val intent = Intent( + "com.google.android.gms.ads.identifier.service.START" + ) + intent.setPackage("com.google.android.gms") + if (context.bindService(intent, connection, Context.BIND_AUTO_CREATE)) { + try { + val adInterface = AdvertisingInterface( + connection.binder + ) + return adInterface.getId() + } finally { + context.unbindService(connection) + } + } + return null + } + + private class AdvertisingConnection : ServiceConnection { + var retrieved: Boolean = false + private val queue = LinkedBlockingQueue(1) + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + try { + queue.put(service) + } catch (localInterruptedException: InterruptedException) { + } + } + + override fun onServiceDisconnected(name: ComponentName) { + } + + @get:Throws(InterruptedException::class) + val binder: IBinder + get() { + check(!this.retrieved) + this.retrieved = true + return queue.take() + } + } + + private class AdvertisingInterface(private val binder: IBinder) : IInterface { + override fun asBinder(): IBinder { + return binder + } + + @Throws(RemoteException::class) + fun getId(): String? { + val data = Parcel.obtain() + val reply = Parcel.obtain() + var id: String? + try { + data.writeInterfaceToken("com.google.android.gms.ads.identifier.internal.IAdvertisingIdService") + binder.transact(1, data, reply, 0) + reply.readException() + id = reply.readString() + } finally { + reply.recycle() + data.recycle() + } + return id + } + + @Throws(RemoteException::class) + fun isLimitAdTrackingEnabled(paramBoolean: Boolean): Boolean { + val data = Parcel.obtain() + val reply = Parcel.obtain() + var limitAdTracking: Boolean + try { + data.writeInterfaceToken("com.google.android.gms.ads.identifier.internal.IAdvertisingIdService") + data.writeInt(if (paramBoolean) 1 else 0) + binder.transact(2, data, reply, 0) + reply.readException() + limitAdTracking = 0 != reply.readInt() + } finally { + reply.recycle() + data.recycle() + } + return limitAdTracking + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/ChangeDeviceInfoUtil.kt b/app/src/main/java/com/android/grape/util/ChangeDeviceInfoUtil.kt new file mode 100644 index 0000000..7df6551 --- /dev/null +++ b/app/src/main/java/com/android/grape/util/ChangeDeviceInfoUtil.kt @@ -0,0 +1,435 @@ +package com.android.grape.util + +import android.content.ContentResolver +import android.content.Context +import android.util.Log +import com.android.grape.data.AfInfo +import com.android.grape.data.BigoInfo +import com.android.grape.data.DeviceInfo +import com.blankj.utilcode.util.LogUtils +import org.json.JSONObject +import java.lang.reflect.InvocationTargetException + +object ChangeDeviceInfoUtil { + fun changeDeviceInfo( + current_pkg_name: String, + context: Context?, + bigoDeviceObject: JSONObject?, + afDeviceObject: JSONObject? + ) { + var bigoDeviceObject = bigoDeviceObject + var afDeviceObject = afDeviceObject + try { + val bigoDevice: BigoInfo + if (bigoDeviceObject != null) { + // BIGO + val cpuClockSpeed = bigoDeviceObject.optString("cpu_clock_speed") + val gaid = bigoDeviceObject.optString("gaid") + val userAgent = bigoDeviceObject.optString("User-Agent") + val osLang = bigoDeviceObject.optString("os_lang") + val osVer = bigoDeviceObject.optString("os_ver") + val tz = bigoDeviceObject.optString("tz") + val systemCountry = bigoDeviceObject.optString("system_country") + val simCountry = bigoDeviceObject.optString("sim_country") + val romFreeIn = bigoDeviceObject.optLong("rom_free_in") + val resolution = bigoDeviceObject.optString("resolution") + val vendor = bigoDeviceObject.optString("vendor") + val batteryScale = bigoDeviceObject.optInt("bat_scale") + // String model = deviceObject.optString("model"); + val net = bigoDeviceObject.optString("net") + val dpi = bigoDeviceObject.optLong("dpi") + val romFreeExt = bigoDeviceObject.optLong("rom_free_ext") + val dpiF = bigoDeviceObject.optString("dpi_f") + val cpuCoreNum = bigoDeviceObject.optLong("cpu_core_num") + + bigoDevice = BigoInfo() + bigoDevice.cpuClockSpeed = cpuClockSpeed + bigoDevice.gaid = gaid + bigoDevice.userAgent = userAgent + bigoDevice.osLang = osLang + bigoDevice.osVer = osVer + bigoDevice.tz = tz + bigoDevice.systemCountry = systemCountry + bigoDevice.simCountry = simCountry + bigoDevice.romFreeIn = romFreeIn + bigoDevice.resolution = resolution + bigoDevice.vendor = vendor + bigoDevice.batteryScale = batteryScale + bigoDevice.net = net + bigoDevice.dpi = dpi + bigoDevice.romFreeExt = romFreeExt + bigoDevice.dpiF = dpiF + bigoDevice.cpuCoreNum = cpuCoreNum +// TaskUtil.setBigoDevice(bigoDevice) + try { + callVCloudSettings_put( + "$current_pkg_name.system_country", + systemCountry, + context + ) + callVCloudSettings_put("$current_pkg_name.sim_country", simCountry, context) + callVCloudSettings_put( + "$current_pkg_name.rom_free_in", + romFreeIn.toString(), + context + ) + callVCloudSettings_put("$current_pkg_name.resolution", resolution, context) + callVCloudSettings_put("$current_pkg_name.vendor", vendor, context) + callVCloudSettings_put( + "$current_pkg_name.battery_scale", + batteryScale.toString(), + context + ) + callVCloudSettings_put("$current_pkg_name.os_lang", osLang, context) + // callVCloudSettings_put(current_pkg_name + ".model", model, context); + callVCloudSettings_put("$current_pkg_name.net", net, context) + callVCloudSettings_put("$current_pkg_name.dpi", dpi.toString(), context) + callVCloudSettings_put( + "$current_pkg_name.rom_free_ext", + romFreeExt.toString(), + context + ) + callVCloudSettings_put("$current_pkg_name.dpi_f", dpiF, context) + callVCloudSettings_put( + "$current_pkg_name.cpu_core_num", + cpuCoreNum.toString(), + context + ) + callVCloudSettings_put( + "$current_pkg_name.cpu_clock_speed", + cpuClockSpeed, + context + ) + callVCloudSettings_put(current_pkg_name + "_gaid", gaid, context) + // **User-Agent** + callVCloudSettings_put(current_pkg_name + "_user_agent", userAgent, context) + // **os_lang**系统语言 + callVCloudSettings_put(current_pkg_name + "_os_lang", osLang, context) + // **os_ver** + callVCloudSettings_put(current_pkg_name + "_os_ver", osVer, context) + // **tz** (时区) + callVCloudSettings_put(current_pkg_name + "_tz", tz, context) + } catch (e: Throwable) { + LogUtils.e( + Log.ERROR, + "ChangeDeviceInfoUtil", + "Error occurred while changing device info", + e + ) + throw RuntimeException("Error occurred in changeDeviceInfo", e) + } + bigoDeviceObject = null + } + + val deviceInfo: DeviceInfo + val afDevice: AfInfo + if (afDeviceObject != null) { + val advertiserId = afDeviceObject.optString(".advertiserId") + val model = afDeviceObject.optString(".model") + val brand = afDeviceObject.optString(".brand") + val androidId = afDeviceObject.optString(".android_id") + val xPixels = afDeviceObject.optInt(".deviceData.dim.x_px") + val yPixels = afDeviceObject.optInt(".deviceData.dim.y_px") + val densityDpi = afDeviceObject.optInt(".deviceData.dim.d_dpi") + val country = afDeviceObject.optString(".country") + val batteryLevel = afDeviceObject.optString(".batteryLevel") + val stackInfo = Thread.currentThread().stackTrace[2].toString() + val product = afDeviceObject.optString(".product") + val network = afDeviceObject.optString(".network") + val langCode = afDeviceObject.optString(".lang_code") + val cpuAbi = afDeviceObject.optString(".deviceData.cpu_abi") + val yDp = afDeviceObject.optLong(".deviceData.dim.ydp") + + afDevice = AfInfo() + afDevice.advertiserId = advertiserId + afDevice.model = model + afDevice.brand = brand + afDevice.androidId = androidId + afDevice.xPixels = xPixels + afDevice.yPixels = yPixels + afDevice.densityDpi = densityDpi + afDevice.country = country + afDevice.batteryLevel = batteryLevel + afDevice.stackInfo = stackInfo + afDevice.product = product + afDevice.network = network + afDevice.langCode = langCode + afDevice.cpuAbi = cpuAbi + afDevice.yDp = yDp +// TaskUtil.setAfDevice(afDevice) + + val lang = afDeviceObject.optString(".lang") + val ro_product_brand = afDeviceObject.optString("ro.product.brand", "") + val ro_product_model = afDeviceObject.optString("ro.product.model", "") + val ro_product_manufacturer = + afDeviceObject.optString("ro.product.manufacturer", "") + val ro_product_device = afDeviceObject.optString("ro.product.device", "") + val ro_product_name = afDeviceObject.optString("ro.product.name", "") + val ro_build_version_incremental = + afDeviceObject.optString("ro.build.version.incremental", "") + val ro_build_fingerprint = afDeviceObject.optString("ro.build.fingerprint", "") + val ro_odm_build_fingerprint = + afDeviceObject.optString("ro.odm.build.fingerprint", "") + val ro_product_build_fingerprint = + afDeviceObject.optString("ro.product.build.fingerprint", "") + val ro_system_build_fingerprint = + afDeviceObject.optString("ro.system.build.fingerprint", "") + val ro_system_ext_build_fingerprint = + afDeviceObject.optString("ro.system_ext.build.fingerprint", "") + val ro_vendor_build_fingerprint = + afDeviceObject.optString("ro.vendor.build.fingerprint", "") + val ro_build_platform = afDeviceObject.optString("ro.board.platform", "") + val persist_sys_cloud_drm_id = + afDeviceObject.optString("persist.sys.cloud.drm.id", "") + val persist_sys_cloud_battery_capacity = + afDeviceObject.optInt("persist.sys.cloud.battery.capacity", -1) + val persist_sys_cloud_gpu_gl_vendor = + afDeviceObject.optString("persist.sys.cloud.gpu.gl_vendor", "") + val persist_sys_cloud_gpu_gl_renderer = + afDeviceObject.optString("persist.sys.cloud.gpu.gl_renderer", "") + val persist_sys_cloud_gpu_gl_version = + afDeviceObject.optString("persist.sys.cloud.gpu.gl_version", "") + val persist_sys_cloud_gpu_egl_vendor = + afDeviceObject.optString("persist.sys.cloud.gpu.egl_vendor", "") + val persist_sys_cloud_gpu_egl_version = + afDeviceObject.optString("persist.sys.cloud.gpu.egl_version", "") + val global_android_id = afDeviceObject.optString(".android_id", "") + val anticheck_pkgs = afDeviceObject.optString(".anticheck_pkgs", "") + val pm_list_features = afDeviceObject.optString(".pm_list_features", "") + val pm_list_libraries = afDeviceObject.optString(".pm_list_libraries", "") + val system_http_agent = afDeviceObject.optString("system.http.agent", "") + val webkit_http_agent = afDeviceObject.optString("webkit.http.agent", "") + val com_fk_tools_pkgInfo = afDeviceObject.optString(".pkg_info", "") + val appsflyerKey = afDeviceObject.optString(".appsflyerKey", "") + val appUserId = afDeviceObject.optString(".appUserId", "") + val disk = afDeviceObject.optString(".disk", "") + val operator = afDeviceObject.optString(".operator", "") + val cell_mcc = afDeviceObject.optString(".cell.mcc", "") + val cell_mnc = afDeviceObject.optString(".cell.mnc", "") + val date1 = afDeviceObject.optString(".date1", "") + val date2 = afDeviceObject.optString(".date2", "") + val bootId = afDeviceObject.optString("BootId", "") + + deviceInfo = DeviceInfo() + deviceInfo.lang = lang + deviceInfo.roProductBrand = ro_product_brand + deviceInfo.roProductModel = ro_product_model + deviceInfo.roProductManufacturer = ro_product_manufacturer + deviceInfo.roProductDevice = ro_product_device + deviceInfo.roProductName = ro_product_name + deviceInfo.roBuildVersionIncremental = ro_build_version_incremental + deviceInfo.roBuildFingerprint = ro_build_fingerprint + deviceInfo.roOdmBuildFingerprint = ro_odm_build_fingerprint + deviceInfo.roProductBuildFingerprint = ro_product_build_fingerprint + deviceInfo.roSystemBuildFingerprint = ro_system_build_fingerprint + deviceInfo.roSystemExtBuildFingerprint = ro_system_ext_build_fingerprint + deviceInfo.roVendorBuildFingerprint = ro_vendor_build_fingerprint + deviceInfo.roBuildPlatform = ro_build_platform + deviceInfo.persistSysCloudDrmId = persist_sys_cloud_drm_id + deviceInfo.persistSysCloudBatteryCapacity = persist_sys_cloud_battery_capacity + deviceInfo.persistSysCloudGpuGlVendor = persist_sys_cloud_gpu_gl_vendor + deviceInfo.persistSysCloudGpuGlRenderer = persist_sys_cloud_gpu_gl_renderer + deviceInfo.persistSysCloudGpuGlVersion = persist_sys_cloud_gpu_gl_version + deviceInfo.persistSysCloudGpuEglVendor = persist_sys_cloud_gpu_egl_vendor + deviceInfo.persistSysCloudGpuEglVersion = persist_sys_cloud_gpu_egl_version +// TaskUtil.setDeviceInfo(deviceInfo) + try { + callVCloudSettings_put("$current_pkg_name.advertiserId", advertiserId, context) + callVCloudSettings_put("$current_pkg_name.model", model, context) + callVCloudSettings_put("$current_pkg_name.brand", brand, context) + callVCloudSettings_put("$current_pkg_name.android_id", androidId, context) + callVCloudSettings_put("$current_pkg_name.lang", lang, context) + callVCloudSettings_put("$current_pkg_name.country", country, context) + callVCloudSettings_put("$current_pkg_name.batteryLevel", batteryLevel, context) + callVCloudSettings_put( + current_pkg_name + "_screen.optMetrics.stack", + stackInfo, + context + ) + callVCloudSettings_put("$current_pkg_name.product", product, context) + callVCloudSettings_put("$current_pkg_name.network", network, context) + callVCloudSettings_put("$current_pkg_name.cpu_abi", cpuAbi, context) + callVCloudSettings_put("$current_pkg_name.lang_code", langCode, context) + // **广告标识符 (advertiserId)** 及 **启用状态** + val isAdIdEnabled = true // 默认启用广告 ID + callVCloudSettings_put( + "$current_pkg_name.advertiserIdEnabled", + isAdIdEnabled.toString(), + context + ) + + val displayMetrics = JSONObject() + + displayMetrics.put("widthPixels", xPixels) + + displayMetrics.put("heightPixels", yPixels) + displayMetrics.put("densityDpi", densityDpi) + displayMetrics.put("yDp", yDp) + callVCloudSettings_put( + "screen.device.displayMetrics", + displayMetrics.toString(), + context + ) + + if (!ShellUtils.hasRootAccess()) { + LogUtils.d( + "ERROR", + "ChangeDeviceInfoUtil", + "Root access is required to execute system property changes" + ) + } + // 设置机型, 直接设置属性 + ShellUtils.execRootCmd("setprop ro.product.brand $ro_product_brand") + ShellUtils.execRootCmd("setprop ro.product.model $ro_product_model") + ShellUtils.execRootCmd("setprop ro.product.manufacturer $ro_product_manufacturer") + ShellUtils.execRootCmd("setprop ro.product.device $ro_product_device") + ShellUtils.execRootCmd("setprop ro.product.name $ro_product_name") + ShellUtils.execRootCmd("setprop ro.build.version.incremental $ro_build_version_incremental") + ShellUtils.execRootCmd("setprop ro.build.fingerprint $ro_build_fingerprint") + ShellUtils.execRootCmd("setprop ro.odm.build.fingerprint $ro_odm_build_fingerprint") + ShellUtils.execRootCmd("setprop ro.product.build.fingerprint $ro_product_build_fingerprint") + ShellUtils.execRootCmd("setprop ro.system.build.fingerprint $ro_system_build_fingerprint") + ShellUtils.execRootCmd("setprop ro.system_ext.build.fingerprint $ro_system_ext_build_fingerprint") + ShellUtils.execRootCmd("setprop ro.vendor.build.fingerprint $ro_vendor_build_fingerprint") + ShellUtils.execRootCmd("setprop ro.board.platform $ro_build_platform") + + // Native.setBootId(bootId); + // 修改drm id + ShellUtils.execRootCmd("setprop persist.sys.cloud.drm.id $persist_sys_cloud_drm_id") + // 电量模拟需要大于1000 + ShellUtils.execRootCmd("setprop persist.sys.cloud.battery.capacity $persist_sys_cloud_battery_capacity") + ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_vendor $persist_sys_cloud_gpu_gl_vendor") + ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_renderer $persist_sys_cloud_gpu_gl_renderer") + // 这个值不能随便改 必须是 OpenGL ES %d.%d 这个格式 + ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_version $persist_sys_cloud_gpu_gl_version") + ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_vendor $persist_sys_cloud_gpu_egl_vendor") + ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_version $persist_sys_cloud_gpu_egl_version") + } catch (e: Throwable) { + LogUtils.e( + Log.ERROR, + "ChangeDeviceInfoUtil", + "Error occurred in changeDeviceInfo", + e + ) + throw RuntimeException("Error occurred in changeDeviceInfo", e) + } + afDeviceObject = null + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun callVCloudSettings_put(key: String?, value: String?, context: Context?) { + if (context == null) { + LogUtils.e(Log.ERROR, "ChangeDeviceInfoUtil", "Context cannot be null", null) + throw IllegalArgumentException("Context cannot be null") + } + if (key == null || key.isEmpty()) { + LogUtils.e(Log.ERROR, "ChangeDeviceInfoUtil", "Key cannot be null or empty", null) + throw IllegalArgumentException("Key cannot be null or empty") + } + if (value == null) { + LogUtils.e(Log.ERROR, "ChangeDeviceInfoUtil", "Value cannot be null", null) + throw IllegalArgumentException("Value cannot be null") + } + + try { + // 获取类对象 + val clazz = Class.forName("android.provider.VCloudSettings\$Global") + val putStringMethod = clazz.getDeclaredMethod( + "putString", + ContentResolver::class.java, + String::class.java, + String::class.java + ) + putStringMethod.isAccessible = true + + // 调用方法 + putStringMethod.invoke(null, context.contentResolver, key, value) + Log.d("Debug", "putString executed successfully.") + } catch (e: ClassNotFoundException) { + LogUtils.e( + Log.WARN, + "ChangeDeviceInfoUtil", + "Class not found: android.provider.VCloudSettings\$Global. This may not be supported on this device.", + e + ) + } catch (e: NoSuchMethodException) { + LogUtils.e( + Log.WARN, + "ChangeDeviceInfoUtil", + "Method not found: android.provider.VCloudSettings\$Global.putString. This may not be supported on this", + e + ) + } catch (e: InvocationTargetException) { + val cause = e.targetException + if (cause is SecurityException) { + LogUtils.e( + Log.ERROR, + "ChangeDeviceInfoUtil", + "Error occurred in changeDeviceInfo", + cause + ) + } else { + LogUtils.e( + Log.ERROR, + "ChangeDeviceInfoUtil", + "Error occurred in changeDeviceInfo", + cause + ) + } + } catch (e: Exception) { + LogUtils.e( + Log.ERROR, + "ChangeDeviceInfoUtil", + "Unexpected error during putString invocation", + e + ) + } + } + +// fun resetChangedDeviceInfo(current_pkg_name: String, context: Context?) { +// try { +// Native.setBootId("00000000000000000000000000000000") +// } catch (e: Exception) { +// LogUtils.e(Log.ERROR, "ChangeDeviceInfoUtil", "Error occurred in reset", e) +// } +// +// if (!ShellUtils.hasRootAccess()) { +// LogUtils.e( +// Log.ERROR, +// "ChangeDeviceInfoUtil", +// "Root access is required to execute system property changes", +// null +// ) +// return +// } +// ShellUtils.execRootCmd("cmd settings2 delete global global_android_id") +// ShellUtils.execRootCmd("cmd settings2 delete global pm_list_features") +// ShellUtils.execRootCmd("cmd settings2 delete global pm_list_libraries") +// ShellUtils.execRootCmd("cmd settings2 delete global anticheck_pkgs") +// ShellUtils.execRootCmd("cmd settings2 delete global " + current_pkg_name + "_android_id") +// ShellUtils.execRootCmd("cmd settings2 delete global " + current_pkg_name + "_adb_enabled") +// ShellUtils.execRootCmd("cmd settings2 delete global " + current_pkg_name + "_development_settings_enabled") +// +// ShellUtils.execRootCmd("setprop persist.sys.cloud.drm.id \"\"") +// +// ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_vendor \"\"") +// ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_renderer \"\"") +// // 这个值不能随便改 必须是 OpenGL ES %d.%d 这个格式 +// ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_version \"\"") +// +// ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_vendor \"\"") +// ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_version \"\"") +// +// ShellUtils.execRootCmd("setprop ro.product.brand Vortex") +// ShellUtils.execRootCmd("setprop ro.product.model HD65_Select") +// ShellUtils.execRootCmd("setprop ro.product.manufacturer Vortex") +// ShellUtils.execRootCmd("setprop ro.product.device HD65_Select") +// ShellUtils.execRootCmd("setprop ro.product.name HD65_Select") +// ShellUtils.execRootCmd("setprop ro.build.version.incremental 20240306") +// ShellUtils.execRootCmd("setprop ro.build.fingerprint \"Vortex/HD65_Select/HD65_Select:13/TP1A.220624.014/20240306:user/release-keys\"") +// ShellUtils.execRootCmd("setprop ro.board.platform sm8150p") +// } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/ClashUtil.kt b/app/src/main/java/com/android/grape/util/ClashUtil.kt new file mode 100644 index 0000000..c0bc5f1 --- /dev/null +++ b/app/src/main/java/com/android/grape/util/ClashUtil.kt @@ -0,0 +1,161 @@ +package com.android.grape.util + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.util.Log +import androidx.core.content.ContextCompat +import com.blankj.utilcode.util.LogUtils +import okhttp3.Call +import okhttp3.Callback +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.util.concurrent.CountDownLatch + +object ClashUtil { + fun startProxy(context: Context) { + val intent = Intent("com.github.kr328.clash.intent.action.SESSION_CREATE") + intent.putExtra("profile", "default") // 可选择您在 Clash 中配置的 Profile + context.sendBroadcast(intent) + } + + fun stopProxy(context: Context) { + Thread { + val intent = Intent("com.github.kr328.clash.intent.action.SESSION_DESTROY") + context.sendBroadcast(intent) + }.start() + } + + var isRunning: Boolean = false + + val clashStatusReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + isRunning = intent.getBooleanExtra("isRunning", false) + Log.d("ClashUtil", "Clash Status: $isRunning") + } + } + + fun checkProxy(context: Context): Boolean { + val latch = CountDownLatch(1) + try { + checkClashStatus(context, latch) + latch.await() // 等待广播接收器更新状态 + } catch (e: InterruptedException) { + LogUtils.log(Log.ERROR, "ClashUtil", "checkProxy: Waiting interrupted", e) + Thread.currentThread().interrupt() // 重新设置中断状态 + return false // 返回默认状态或尝试重试 + } + return isRunning + } + + fun checkClashStatus(context: Context, latch: CountDownLatch) { + val intentFilter = IntentFilter("com.github.kr328.clash.intent.action.SESSION_STATE") + val clashStatusReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + isRunning = intent.getBooleanExtra("isRunning", false) + Log.d("ClashUtil", "Clash Status: $isRunning") + latch.countDown() // 状态更新完成,释放锁 + } + } + ContextCompat.registerReceiver( + context, + clashStatusReceiver, + intentFilter, + ContextCompat.RECEIVER_NOT_EXPORTED + ) + val queryIntent = Intent("com.github.kr328.clash.intent.action.SESSION_QUERY") + context.sendBroadcast(queryIntent) + } + + fun unregisterReceiver(context: Context) { + context.unregisterReceiver(clashStatusReceiver) + } + + fun switchProxyGroup(groupName: String?, proxyName: String?, controllerUrl: String) { + if (groupName == null || groupName.trim { it <= ' ' } + .isEmpty() || proxyName == null || proxyName.trim { it <= ' ' }.isEmpty()) { + LogUtils.log( + Log.ERROR, + "ClashUtil", + "switchProxyGroup: Invalid arguments", + null + ) + throw IllegalArgumentException("Group name and proxy name must not be empty") + } + if (!controllerUrl.matches("^https?://.*".toRegex())) { + LogUtils.log( + Log.ERROR, + "ClashUtil", + "switchProxyGroup: Invalid controller URL", + null + ) + throw IllegalArgumentException("Invalid controller URL") + } + + val client = OkHttpClient() + val json = JSONObject() + try { + json.put("name", proxyName) + } catch (e: JSONException) { + LogUtils.log(Log.ERROR, "ClashUtil", "switchProxyGroup: JSON error", e) + } + val jsonBody = json.toString() + + val JSON: MediaType = "application/json; charset=utf-8".toMediaType() + val requestBody = jsonBody.toRequestBody(JSON) + + val url = controllerUrl.toHttpUrl() + .newBuilder() + .addPathSegments("proxies/$groupName") + .build() + + val request = Request.Builder() + .url(url) + .put(requestBody) + .build() + + client.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + LogUtils.log( + Log.ERROR, + "ClashUtil", + "switchProxyGroup: Failed to switch proxy", + e + ) + println("Failed to switch proxy: " + e.message) + } + + @Throws(IOException::class) + override fun onResponse(call: Call, response: Response) { + try { + if (response.body != null) { + LogUtils.log( + Log.INFO, + "ClashUtil", + "switchProxyGroup: Switch proxy response", + null + ) + } else { + LogUtils.log( + Log.ERROR, + "ClashUtil", + "switchProxyGroup: Response body is null", + null + ) + } + } finally { + response.close() + } + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/DeviceConvertUtil.kt b/app/src/main/java/com/android/grape/util/DeviceConvertUtil.kt new file mode 100644 index 0000000..4384647 --- /dev/null +++ b/app/src/main/java/com/android/grape/util/DeviceConvertUtil.kt @@ -0,0 +1,247 @@ +package com.android.grape.util + +import android.util.Log +import org.json.JSONException +import org.json.JSONObject +import java.util.Random +import java.util.UUID + +object DeviceConvertUtil { + @Throws(JSONException::class) + fun MGConvert(temple: JSONObject): JSONObject { + val result = JSONObject() + result.put("sensor_size", temple.getInt("sensor_size")) + //result.put("pr",temple.getJSONObject("pr").getJSONObject("nameValuePairs")); + result.put("sensor", temple.getJSONArray("sensor")) + + result.put("networkType", getNetworkType()) // + result.put("brand", temple.getString("brand")) + if (temple.has("cpu_abi")) { + result.put("ABI", temple.getString("cpu_abi")) + } + + if (temple.has("cpu_abi2")) { + result.put("ABI2", temple.getString("cpu_abi2")) + } + + val drmId = UUID.randomUUID().toString().replace("-", "") + val sysBootId = UUID.randomUUID().toString() + result.put("drmId", drmId) + result.put("sysBootId", sysBootId) + result.put("screenBrightness", Random().nextInt(100)) + + + result.put("device", temple.getString("device")) + result.put("display", temple.getString("build_display_id")) + result.put("model", temple.getString("model")) + result.put("product", temple.getString("product")) + result.put("rawProduct", temple.getString("rawProduct")) + result.put("rawDevice", temple.getString("rawDevice")) + + + result.put("API", temple.getString("sdk")) + result.put("AndroidID", temple.getString("androidId")) + result.put("Carrier", temple.getString("carrier")) // + result.put("simopename", temple.getString("operator")) // + result.put("gaid", temple.getString("advertiserId")) // + result.put("regionid", temple["deviceCity"]) //timezoneJo.get("deviceCity")); + result.put("lang", temple.getString("lang_code")) //Locale.lang + result.put("displayLang", temple.getString("lang")) //Locale.displayLang + result.put("country", temple.getString("country")) //Locale.country + result.put("vendingVersionCode", DeviceConvertUtil.getLong(temple, "vendingVersionCode")) + result.put("vendingVersionName", DeviceConvertUtil.getString(temple, "vendingVersionName")) + result.put("build_type", temple.getString("deviceType")) + result.put("osArch", DeviceConvertUtil.getString(temple, "arch")) + + val dim = temple.getJSONObject("dim") + result.put("width", dim.getInt("x_px")) + result.put("height", dim.getInt("y_px")) + result.put("DPI", dim.getInt("d_dpi")) + result.put("xdpi", dim.getDouble("xdp")) + result.put("ydpi", dim.getDouble("xdp")) + result.put("screenLayout", dim.getInt("size")) + + + result.put("wvua", temple.getString("sys_ua")) + /**web_ua */ + var forwardIp = "" + if (temple.has("ip") && !temple.isNull("ip")) { + forwardIp = temple.getString("ip") + } + result.put("forwardIp", forwardIp) + + + val pr = temple.getJSONObject("pr").getJSONObject("nameValuePairs") + + //result.put("armVariant", temple.getString("armVariant")); + result.put("roArch", getString(pr, "aa")) + result.put("chipName", getString(pr, "ab")) + result.put("nativeBridge", getString(pr, "ac")) + result.put("sysNativeBridge", getString(pr, "ad")) + result.put("nativeBridgeExec", getString(pr, "ae")) + result.put("x86Features", getString(pr, "af")) + result.put("x86Variant", getString(pr, "ag")) + result.put("zygote", getString(pr, "ah")) + result.put("mockLocation", getString(pr, "ai")) + result.put("isaArm", getString(pr, "aj")) + result.put("armFeatures", getString(pr, "ak")) + result.put("armVariant", getString(pr, "al")) + result.put("armFeatures", getString(pr, "am")) //am + result.put("armVariant", getString(pr, "an")) //an + result.put("", getString(pr, "ao")) //ao + result.put("buildUser", getString(pr, "ap")) //ap + result.put("", getString(pr, "aq")) //aq + result.put("NAME", getString(pr, "ar")) //ar + result.put("ABI", getString(pr, "as")) //as + result.put("abiList", getString(pr, "at")) //at + result.put("abiList32", getString(pr, "au")) //au + result.put("abiList64", getString(pr, "av")) //av + + //as + val `as` = temple.getJSONObject("as").getJSONObject("nameValuePairs") + result.put("InstallerPackageCountOfNone", getInt(`as`, "null")) + result.put("InstallerPackageCountOfOther", getInt(`as`, "other")) + result.put("InstallerPackageCountOfVending", getInt(`as`, "cav")) + + //disk + val disk = temple.getString("disk") + val disk_arr = disk.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + result.put("diskAvailableBlocks", disk_arr[0].toLong()) + result.put("diskBlockSize", disk_arr[1].toLong()) + result.put("diskBlockCount", 1) + + //batteryLevel + result.put("batteryLevel", temple.getInt("batteryLevel")) // + + //battery + result.put( + "batteryTempratureMinValue", + (getInt(temple, "battery") * 0.9).toInt() + ) + result.put( + "batteryTempratureMaxValue", + (getInt(temple, "battery") * 1.1).toInt() + ) + + //telephony + result.put("mcc", temple.getInt("mcc")) + result.put("mnc", temple.getInt("mnc")) + + result.put("firstInstallTime", 0L) + + if (temple.has("sensors")) { + result.put("sensors", temple.getString("sensors")) + } + + if (temple.has("expand")) { + val expand = temple.getJSONObject("expand") + // result.put("expand", expand); + result.put("board", expand.getString("board")) + result.put("longtitude", expand.getDouble("longtitude")) + result.put("latitude", expand.getDouble("latitude")) + Log.d("IOSTQ:longtitude", expand.getDouble("longtitude").toString() + "") + result.put("fingerprint", expand.getString("fingerprint")) + result.put("Manufacture", expand.getString("Manufacture")) + result.put("ID", expand.getString("ID")) + result.put("roBuildDescription", expand.getString("roBuildDescription")) + result.put("WifiMAC", expand.getString("WifiMAC")) + result.put("WifiName", expand.getString("WifiName")) + result.put("BSSID", expand.getString("BSSID")) + result.put("tmdisplayname", expand.getString("tmdisplayname")) + result.put("timerawoff", expand.getString("timerawoff")) + result.put("displayLang", expand.getString("displayLang")) + result.put("cpuinfostr", expand.getString("cpuinfostr")) + result.put("cpuinfobuf", expand.getString("cpuinfobuf")) + result.put("pmlib", expand.getString("pmlib")) + result.put("pmfea", expand.getString("pmfea")) + result.put("soRomRam", expand.getString("soRomRam")) + result.put("roHardware", expand.getString("roHardware")) + result.put("AndroidVer", expand.getString("AndroidVer")) + + result.put("WifiMAC", expand.getString("WifiMAC")) + result.put("ratioVersion", expand.getString("ratioVersion")) + result.put("sdcardCreateTime", expand.getString("sdcardCreateTime")) + result.put("localip", expand.getString("localip")) + result.put("incremental", expand.getString("incremental")) + result.put("amGetConfig", expand.getString("amGetConfig")) + result.put("roBuildDescription", expand.getString("roBuildDescription")) + + + result.put("rootStatfs", expand.getString("rootStatfs")) + result.put("downloadCacheStatfs", expand.getString("downloadCacheStatfs")) + result.put("dataStatfs", expand.getString("dataStatfs")) + result.put("sdStatfs", expand.getString("sdStatfs")) + result.put("baseband", expand.getString("baseband")) + result.put("procVersion", expand.getString("procVersion")) + result.put("procStat", expand.getString("procStat")) + result.put("glRenderer", expand.getString("glRenderer")) + result.put("glVendor", expand.getString("glVendor")) + result.put("glVersion", expand.getString("glVersion")) + result.put("glExtensions", expand.getString("glExtensions")) + } + + + return result + } + + fun getDouble(jo: JSONObject, name: String): Double { + if (jo.has(name) && !jo.isNull(name)) { + try { + return jo.getDouble(name) + } catch (e: Exception) { + } + } + + return 0.0 + } + + fun getBoolean(jo: JSONObject, name: String): Boolean { + if (jo.has(name) && !jo.isNull(name)) { + try { + return jo.getBoolean(name) + } catch (e: Exception) { + } + } + + return false + } + + fun getInt(jo: JSONObject, name: String): Int { + if (jo.has(name) && !jo.isNull(name)) { + try { + return jo.getInt(name) + } catch (e: Exception) { + } + } + + return 0 + } + + fun getLong(jo: JSONObject, name: String): Long { + if (jo.has(name) && !jo.isNull(name)) { + try { + return jo.getLong(name) + } catch (e: Exception) { + } + } + + return 0L + } + + fun getString(jo: JSONObject, name: String): String { + if (jo.has(name) && !jo.isNull(name)) { + try { + return jo.getString(name) + } catch (e: Exception) { + } + } + + return "" + } + + val networkTypeArr: IntArray = intArrayOf(1, 3, 4) + + private fun getNetworkType(): Int { + return networkTypeArr[Random().nextInt(networkTypeArr.size)] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/FileUtils.kt b/app/src/main/java/com/android/grape/util/FileUtils.kt new file mode 100644 index 0000000..a266b3b --- /dev/null +++ b/app/src/main/java/com/android/grape/util/FileUtils.kt @@ -0,0 +1,390 @@ +package com.android.grape.util + +import android.util.Log +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.FileWriter +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.io.OutputStream +import java.io.RandomAccessFile +import java.io.UnsupportedEncodingException +import java.util.Enumeration +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + + +/** + * @author litianxiang + * @description: + * @date :2021/9/28 15:44 + */ +object FileUtils { + private const val TAG = "IOSTQ:FileUtils" + private const val BUFFER_SIZE = 1024 * 1024 //1M Byte + + + //创建文件夹 + fun createDir(destDirName: String): Boolean { + var destDirName = destDirName + val dir = File(destDirName) + if (dir.exists()) { + println("创建目录" + destDirName + "失败,目标目录已经存在") + return false + } + if (!destDirName.endsWith(File.separator)) { + destDirName = destDirName + File.separator + } + //创建目录 + if (dir.mkdirs()) { + Log.e("createDir", "创建目录" + destDirName + "成功!") + return true + } else { + Log.e("createDir", "创建目录" + destDirName + "失败!") + return false + } + } + + + /** + * 创建文件 + * + * @param filePath 文件地址 + * @param fileName 文件名 + * @return + */ + fun createFile(filePath: String, fileName: String): Boolean { + val strFilePath = filePath + fileName + + val file = File(filePath) + if (!file.exists()) { + /** 注意这里是 mkdirs()方法 可以创建多个文件夹 */ + file.mkdirs() + } + + val subfile = File(strFilePath) + + if (!subfile.exists()) { + try { + val b = subfile.createNewFile() + return b + } catch (e: IOException) { + e.printStackTrace() + } + } else { + return true + } + return false + } + + /** + * 遍历文件夹下的文件 + * + * @param file 地址 + */ + fun getFile(file: File): List? { + val list: MutableList = mutableListOf() + val fileArray = file.listFiles() + if (fileArray == null) { + return null + } else { + for (f in fileArray) { + if (f.isFile) { + list.add(0, f) + } else { + getFile(f) + } + } + } + return list + } + + /** + * 删除文件 + * + * @param filePath 文件地址 + * @return + */ + fun deleteFiles(filePath: String): Boolean { + val files = getFile(File(filePath)) + if (files!!.size != 0) { + for (i in files.indices) { + val file = files[i] + + /** 如果是文件则删除 如果都删除可不必判断 */ + if (file.isFile) { + file.delete() + } + } + } + return true + } + + //删除文件夹和文件夹里面的文件 + fun deleteDirWihtFile(dir: File?) { + if (dir == null || !dir.exists() || !dir.isDirectory) return + for (file in dir.listFiles()) { + if (file.isFile) file.delete() // 删除所有文件 + else if (file.isDirectory) deleteDirWihtFile(file) // 递规的方式删除文件夹 + } + dir.delete() // 删除目录本身 + } + + + /** + * 向文件中添加内容 + * + * @param strcontent 内容 + * @param filePath 地址 + * @param fileName 文件名 + */ + fun writeToFile(strcontent: String, filePath: String, fileName: String) { + //生成文件夹之后,再生成文件,不然会出错 + val strFilePath = filePath + fileName + + // 每次写入时,都换行写 + val subfile = File(strFilePath) + + + var raf: RandomAccessFile? = null + try { + /** 构造函数 第二个是读写方式 */ + raf = RandomAccessFile(subfile, "rw") + /** 将记录指针移动到该文件的最后 */ + raf.seek(subfile.length()) + /** 向文件末尾追加内容 */ + raf.write(strcontent.toByteArray()) + + raf.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + + + /** + * 修改文件内容(覆盖或者添加) + * + * @param path 文件地址 + * @param content 覆盖内容 + * @param append 指定了写入的方式,是覆盖写还是追加写(true=追加)(false=覆盖) + */ + fun modifyFile(path: String?, content: String?, append: Boolean) { + try { + val fileWriter = FileWriter(path, append) + val writer = BufferedWriter(fileWriter) + writer.append(content) + writer.flush() + writer.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + + /** + * 读取文件内容 + * + * @param filePath 地址 + * @param filename 名称 + * @return 返回内容 + */ + fun getString(filePath: String, filename: String): String { + var inputStream: FileInputStream? = null + try { + inputStream = FileInputStream(File(filePath + filename)) + } catch (e: FileNotFoundException) { + e.printStackTrace() + } + var inputStreamReader: InputStreamReader? = null + try { + inputStreamReader = InputStreamReader(inputStream, "UTF-8") + } catch (e1: UnsupportedEncodingException) { + e1.printStackTrace() + } + val reader = BufferedReader(inputStreamReader) + val sb = StringBuffer("") + var line: String? + try { + while ((reader.readLine().also { line = it }) != null) { + sb.append(line) + sb.append("\n") + } + } catch (e: IOException) { + e.printStackTrace() + } + return sb.toString() + } + + /** + * 重命名文件 + * + * @param oldPath 原来的文件地址 + * @param newPath 新的文件地址 + */ + fun renameFile(oldPath: String, newPath: String) { + val oleFile = File(oldPath) + val newFile = File(newPath) + //执行重命名 + oleFile.renameTo(newFile) + } + + + /** + * 复制文件 + * + * @param fromFile 要复制的文件目录 + * @param toFile 要粘贴的文件目录 + * @return 是否复制成功 + */ + fun copy(fromFile: String, toFile: String): Boolean { + //要复制的文件目录 + val currentFiles: Array + val root = File(fromFile) + //如同判断SD卡是否存在或者文件是否存在 + //如果不存在则 return出去 + if (!root.exists()) { + return false + } + //如果存在则获取当前目录下的全部文件 填充数组 + currentFiles = root.listFiles() + + //目标目录 + val targetDir = File(toFile) + //创建目录 + if (!targetDir.exists()) { + targetDir.mkdirs() + } + //遍历要复制该目录下的全部文件 + for (i in currentFiles.indices) { + if (currentFiles[i].isDirectory) //如果当前项为子目录 进行递归 + { + copy(currentFiles[i].path + "/", toFile + currentFiles[i].name + "/") + } else //如果当前项为文件则进行文件拷贝 + { + CopySdcardFile(currentFiles[i].path, toFile + currentFiles[i].name) + } + } + return true + } + + + //文件拷贝 + //要复制的目录下的所有非子目录(文件夹)文件拷贝 + fun CopySdcardFile(fromFile: String?, toFile: String?): Boolean { + try { + val fosfrom: InputStream = FileInputStream(fromFile) + val fosto: OutputStream = FileOutputStream(toFile) + val bt = ByteArray(1024) + var c: Int + while ((fosfrom.read(bt).also { c = it }) > 0) { + fosto.write(bt, 0, c) + } + fosfrom.close() + fosto.close() + return true + } catch (ex: Exception) { + return false + } + } + + /** + * 获取目录下所有文件 + * @param path 指定目录路径 + * @return + */ + fun getFilesAllName(path: String): List? { + val file = File(path) + val files = file.listFiles() + if (files == null) { + Log.e("error", "空目录") + return null + } + val s: MutableList = ArrayList() + for (i in files.indices) { + s.add(files[i].absolutePath) + } + return s + } + + + /** + * 解压缩一个文件 + * + * @param zipFile 压缩文件 + * @param folderPath 解压缩的目标目录 + * @return + * @throws IOException 当解压缩过程出错时抛出 + */ + @Throws(IOException::class) + fun upZipFile(zipFile: File?, folderPath: String): ArrayList { + val fileList = ArrayList() + val desDir = File(folderPath) + if (!desDir.exists()) { + desDir.mkdirs() + } + val zf = ZipFile(zipFile) + val entries: Enumeration<*> = zf.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() as ZipEntry + if (entry.isDirectory) { + continue + } + val `is` = zf.getInputStream(entry) + var str = folderPath + File.separator + entry.name + str = String(str.toByteArray(charset("8859_1")), charset("UTF-8")) + val desFile = File(str) + if (!desFile.exists()) { + val fileParentDir = desFile.parentFile + if (!fileParentDir!!.exists()) { + fileParentDir.mkdirs() + } + desFile.createNewFile() + } + val os: OutputStream = FileOutputStream(desFile) + val buffer = ByteArray(BUFFER_SIZE) + var length: Int + while ((`is`.read(buffer).also { length = it }) > 0) { + os.write(buffer, 0, length) + } + os.flush() + os.close() + `is`.close() + fileList.add(desFile) + } + return fileList + } + + + //复制文件 + @Throws(IOException::class) + fun copyFile(sourceFile: File?, targetFile: File?) { + // 新建文件输入流并对它进行缓冲 + val input = FileInputStream(sourceFile) + val inBuff = BufferedInputStream(input) + + // 新建文件输出流并对它进行缓冲 + val output = FileOutputStream(targetFile) + val outBuff = BufferedOutputStream(output) + + // 缓冲数组 + val b = ByteArray(1024 * 5) + var len: Int + while ((inBuff.read(b).also { len = it }) != -1) { + outBuff.write(b, 0, len) + } + // 刷新此缓冲的输出流 + outBuff.flush() + //关闭流 + inBuff.close() + outBuff.close() + output.close() + input.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/MockTools.kt b/app/src/main/java/com/android/grape/util/MockTools.kt new file mode 100644 index 0000000..b866c87 --- /dev/null +++ b/app/src/main/java/com/android/grape/util/MockTools.kt @@ -0,0 +1,67 @@ +package com.android.grape.util + +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.InputStreamReader +import java.io.OutputStreamWriter +import java.net.InetSocketAddress +import java.net.Socket + + +object MockTools { + fun exec(cmd: String): String { + var retString = "" + try { + //创建socket + val myCmd = "SU|$cmd" + + val mSocket = Socket() + val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345) + mSocket.connect(inetSocketAddress) + val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream())) + val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream())) + bufferedWriter.write(myCmd + "\r\n") + bufferedWriter.flush() + val stringBuilder = StringBuilder() + var line: String? = null + while ((bufferedReader.readLine().also { line = it }) != null) { + stringBuilder.append(line + "\n") + } + //retString = bufferedReader.readLine(); + retString = stringBuilder.toString() + bufferedReader.close() + bufferedWriter.close() + } catch (eeeee: Exception) { + eeeee.printStackTrace() + } + return retString + } + + fun execRead(cmd: String): String { + var retString = "" + try { + //创建socket + val myCmd = "SU_1|$cmd" + + val mSocket = Socket() + val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345) + mSocket.connect(inetSocketAddress) + val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream())) + val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream())) + bufferedWriter.write(myCmd + "\r\n") + bufferedWriter.flush() + val stringBuilder = StringBuilder() + var line: String? = null + while ((bufferedReader.readLine().also { line = it }) != null) { + stringBuilder.append(line + "\n") + } + retString = stringBuilder.toString() + //retString = bufferedReader.readLine(); + bufferedReader.close() + bufferedWriter.close() + } catch (eeeee: Exception) { + eeeee.printStackTrace() + } + return retString + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/ReflectionHelper.kt b/app/src/main/java/com/android/grape/util/ReflectionHelper.kt new file mode 100644 index 0000000..773d5a5 --- /dev/null +++ b/app/src/main/java/com/android/grape/util/ReflectionHelper.kt @@ -0,0 +1,61 @@ +package com.android.grape.util + +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi + +object ReflectionHelper { + /** + * 安全地调用私有方法 + * @param instance 目标对象实例 + * @param methodName 方法名 + * @param paramTypes 参数类型列表 + * @param args 实际参数 + * @return 调用结果 + */ + fun callPrivateMethod( + instance: Any, + methodName: String, + paramTypes: Array>, + vararg args: Any? + ): T? { + return try { + val method = instance.javaClass.getDeclaredMethod(methodName, *paramTypes) + method.isAccessible = true + method.invoke(instance, *args) as? T + } catch (e: Exception) { + handleReflectionError(e) + null + } + } + + private fun handleReflectionError(e: Exception) { + when (e) { + is NoSuchMethodException -> + Log.w("Reflection", "Method not found", e) + is IllegalAccessException -> + Log.w("Reflection", "Access denied", e) + else -> + Log.e("Reflection", "Reflection error", e) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // Android 9+ 反射限制检查 + reportNonSdkApiUsage() + } + } + + @RequiresApi(Build.VERSION_CODES.P) + private fun reportNonSdkApiUsage() { + // 使用 Android 的 API 报告工具 +// VMRuntime.getRuntime().setHiddenApiExemptions(listOf("Lcom/example/").toTypedArray()) + } +} + +// 使用示例 +val result = ReflectionHelper.callPrivateMethod( + instance = "", + methodName = "getHiddenData", + paramTypes = arrayOf(), + args = arrayOf() +) \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/ScriptUtil.kt b/app/src/main/java/com/android/grape/util/ScriptUtil.kt new file mode 100644 index 0000000..c7f445c --- /dev/null +++ b/app/src/main/java/com/android/grape/util/ScriptUtil.kt @@ -0,0 +1,25 @@ +package com.android.grape.util + +import android.content.Context +import android.content.Intent +import android.util.Log + +object ScriptUtil { + const val TAG: String = "LU_SCRIPT" + const val START: Int = 1 + const val STOP: Int = 2 + const val SRC_FILES: String = "sc_file" + const val SC_ACTION: String = "com.ak.lu.SCRIPT" + const val EVENT: String = "event" + + fun execScript(context: Context, src: String?) { + val intent = Intent(SC_ACTION) + // todo 需要替换为调度app的pkg +// intent.setComponent(new ComponentName(TARGET_PKG, ScriptReceiver.class.getName())); + intent.putExtra(EVENT, START) + intent.putExtra(SRC_FILES, src) + context.sendBroadcast(intent) + Util.script_status = 0 + Log.i(TAG, context.packageName + " send broadcast:" + SC_ACTION + ":" + EVENT) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/ServiceUtils.kt b/app/src/main/java/com/android/grape/util/ServiceUtils.kt new file mode 100644 index 0000000..8bc4b08 --- /dev/null +++ b/app/src/main/java/com/android/grape/util/ServiceUtils.kt @@ -0,0 +1,114 @@ +package com.android.grape.util + +import android.app.IMikRom +import android.os.IBinder +import android.util.Log + +object ServiceUtils { + private var iMikRom: IMikRom? = null + + fun getiMikRom(): IMikRom? { + if (iMikRom == null) { + try { + val localClass = Class.forName("android.os.ServiceManager") + val getService = localClass.getMethod( + "getService", *arrayOf>( + String::class.java + ) + ) + if (getService != null) { + val objResult = getService.invoke(localClass, *arrayOf("mikrom")) + if (objResult != null) { + val binder = objResult as IBinder + iMikRom = IMikRom.Stub.asInterface(binder) + } + } + } catch (e: Exception) { + Log.d("MikManager", e.message!!) + e.printStackTrace() + } + } + return iMikRom + } + + /** + * 文件读取 + * @param path + * @return + */ + fun readFile(path: String?): String { + var retMsg = "" + val iMikRom: IMikRom? = getiMikRom() + if (iMikRom != null) { + try { + retMsg = iMikRom.readFile(path) + } catch (eee: Exception) { + eee.printStackTrace() + } + } + return retMsg + } + + /** + * 文件保存 + * @param path + * @param data + */ + fun writeFile(path: String?, data: String?) { + val iMikRom: IMikRom? = getiMikRom() + if (iMikRom != null) { + try { + iMikRom.writeFile(path, data) + } catch (eee: Exception) { + eee.printStackTrace() + } + } + } + + fun shellExec(cmd: String?): String { + var retMsg = "" + val iMikRom: IMikRom? = getiMikRom() + if (iMikRom != null) { + try { + retMsg = iMikRom.shellExec(cmd) + } catch (eee: Exception) { + eee.printStackTrace() + } + } + return retMsg + } + + /** + * 判断App是否已经开启注入 + * @param callingPkg + * @return + */ + fun isEnableApp(callingPkg: String): Boolean { + var retMsg = false + val iMikRom: IMikRom? = getiMikRom() + if (iMikRom != null) { + try { + retMsg = iMikRom.isEnableApp(callingPkg) + } catch (eee: Exception) { + eee.printStackTrace() + } + } + return retMsg + } + + /** + * 设置App是否需要打开注入 + * @param pkgName + * @param isEnable + */ + fun setEnableApp(pkgName: String, isEnable: Boolean) { + val iMikRom: IMikRom? = getiMikRom() + if (iMikRom != null) { + try { + iMikRom.setEnableApp(pkgName, isEnable) + } catch (eee: Exception) { + eee.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/ShellUtils.java b/app/src/main/java/com/android/grape/util/ShellUtils.java new file mode 100644 index 0000000..3082e1f --- /dev/null +++ b/app/src/main/java/com/android/grape/util/ShellUtils.java @@ -0,0 +1,382 @@ +package com.android.grape.util; + +import static java.security.AccessController.getContext; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import com.blankj.utilcode.util.LogUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class ShellUtils { + + public static String getPackagePath(Context context, String packageName) { + try { + PackageManager pm = context.getPackageManager(); + ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0); + return appInfo.sourceDir; // 返回 APK 的完整路径 + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return ""; + } + } + + public static void exec(String cmd) { + try { + LogUtils.e(Log.INFO, "ShellUtils", "Executing command: " + cmd, null); + Process process = Runtime.getRuntime().exec(cmd); + process.waitFor(); + } catch (Exception e) { + LogUtils.e(Log.ERROR, "ShellUtils", "Error executing command: " + e.getMessage(), e); + } + } + + public static int getPid(Process p) { + int pid = -1; + try { + Field f = p.getClass().getDeclaredField("pid"); + f.setAccessible(true); + pid = f.getInt(p); + f.setAccessible(false); + } catch (Throwable e) { + pid = -1; + } + return pid; + } + + public static boolean hasBin(String binName) { + // 验证 binName 是否符合规则 + if (binName == null || binName.isEmpty()) { + LogUtils.e(Log.ERROR, "ShellUtils", "Invalid bin name",null); + throw new IllegalArgumentException("Bin name cannot be null or empty"); + } + + for (char c : binName.toCharArray()) { + if (!Character.isLetterOrDigit(c) && c != '.' && c != '_' && c != '-') { + throw new IllegalArgumentException("Invalid bin name"); + } + } + + // 获取 PATH 环境变量 + String pathEnv = System.getenv("PATH"); + if (pathEnv == null) { + LogUtils.e(Log.ERROR, "ShellUtils", "PATH environment variable is not available", null); + return false; + } + + // 使用适合当前系统的路径分隔符分割路径 + String[] paths = pathEnv.split(File.pathSeparator); + for (String path : paths) { + File file = new File(path, binName); // 使用 File 构造完整路径 + try { + // 检查文件是否可执行 + if (file.canExecute()) { + return true; + } + } catch (SecurityException e) { + LogUtils.e(Log.ERROR, "ShellUtils", "Security exception occurred while checking: " + file.getAbsolutePath(), e); + } + } + + // 如果未找到可执行文件,返回 false + return false; + } + + public static String execRootCmdAndGetResult(String cmd) { + Log.d("ShellUtils", "execRootCmdAndGetResult - Started execution for command: " + cmd); + if (cmd == null || cmd.trim().isEmpty()) { + LogUtils.e(Log.ERROR, "ShellUtils", "Unsafe or empty command. Aborting execution.", null); + throw new IllegalArgumentException("Unsafe or empty command."); + } + // if (!isCommandSafe(cmd)) { // 检查命令的合法性 + // Log.e("ShellUtils", "Detected unsafe command. Aborting execution."); + // throw new IllegalArgumentException("Detected unsafe command."); + // } + + Process process = null; + ExecutorService executor = Executors.newFixedThreadPool(2); + + try { + Log.d("ShellUtils", "Determining appropriate shell for execution..."); + if (hasBin("su")) { + Log.d("ShellUtils", "'su' binary found, using 'su' shell."); + process = Runtime.getRuntime().exec("su"); + } else if (hasBin("xu")) { + Log.d("ShellUtils", "'xu' binary found, using 'xu' shell."); + process = Runtime.getRuntime().exec("xu"); + } else if (hasBin("vu")) { + Log.d("ShellUtils", "'vu' binary found, using 'vu' shell."); + process = Runtime.getRuntime().exec("vu"); + } else { + Log.d("ShellUtils", "No specific binary found, using 'sh' shell."); + process = Runtime.getRuntime().exec("sh"); + } + + try (OutputStream os = new BufferedOutputStream(process.getOutputStream()); + InputStream is = process.getInputStream(); + InputStream errorStream = process.getErrorStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream, StandardCharsets.UTF_8))) { + + Log.d("ShellUtils", "Starting separate thread to process error stream..."); + executor.submit(() -> { + String line; + try { + while ((line = errorReader.readLine()) != null) { + LogUtils.e(Log.ERROR, "ShellUtils", "Shell Error: " + line, null); + } + } catch (IOException e) { + LogUtils.e(Log.ERROR, "ShellUtils", "Error while reading process error stream: " + e.getMessage(), e); + } + }); + + Log.d("ShellUtils", "Writing the command to the shell..."); + os.write((cmd + "\n").getBytes()); + os.write("exit\n".getBytes()); + os.flush(); + Log.d("ShellUtils", "Command written to shell. Waiting for process to complete."); + + StringBuilder output = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + Log.d("ShellUtils", "Shell Output: " + line); + output.append(line).append("\n"); + } + + Log.d("ShellUtils", "Awaiting process termination..."); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (!process.waitFor(10, TimeUnit.SECONDS)) { + LogUtils.e(Log.ERROR, "ShellUtils", "Process execution timed out. Destroying process.", null); + process.destroyForcibly(); + throw new RuntimeException("Shell command execution timeout."); + } + } else { + Log.d("ShellUtils", "Using manual time tracking method for process termination (API < 26)."); + long startTime = System.currentTimeMillis(); + while (true) { + try { + process.exitValue(); + break; + } catch (IllegalThreadStateException e) { + if (System.currentTimeMillis() - startTime > 10000) { // 10 seconds + LogUtils.e(Log.ERROR, "ShellUtils", "Process execution timed out (manual tracking). Destroying process.", null); + process.destroy(); + throw new RuntimeException("Shell command execution timeout."); + } + Thread.sleep(100); // Sleep briefly before re-checking + } + } + } + + Log.d("ShellUtils", "Process terminated successfully. Returning result."); + return output.toString().trim(); + } + } catch (IOException | InterruptedException e) { + LogUtils.e(Log.ERROR, "ShellUtils", "Command execution failed: " + e.getMessage(), e); + Thread.currentThread().interrupt(); + return "Error: " + e.getMessage(); + } finally { + if (process != null) { + Log.d("ShellUtils", "Finalizing process. Attempting to destroy it."); + process.destroy(); + } + executor.shutdown(); + Log.d("ShellUtils", "Executor service shut down."); + } + } + + public static void execRootCmd(String cmd) { + // 校验命令是否安全 + if (!isCommandSafe(cmd)) { + LogUtils.e(Log.ERROR, "ShellUtils", "Unsafe command, aborting.", null); + return; + } + List cmds = new ArrayList<>(); + cmds.add(cmd); + + // 使用同步锁保护线程安全 + synchronized (ShellUtils.class) { + try { + List results = execRootCmds(cmds); + // 判断是否需要打印输出,仅用于开发调试阶段 + for (String result : results) { + Log.d("ShellUtils", "Command Result: " + result); + } + } catch (Exception e) { + LogUtils.e(Log.ERROR, "ShellUtils", "Unexpected error: " + e.getMessage(), e); + } + } + } + + + private static boolean isCommandSafe(String cmd) { + // 检查空值和空字符串 + if (cmd == null || cmd.trim().isEmpty()) { + LogUtils.e(Log.ERROR, "ShellUtils", "Rejected command: empty or null value.", null); + return false; + } + + // 检查非法字符 + if (!cmd.matches("^[a-zA-Z0-9._/:\\-~`'\" *|]+$")) { + LogUtils.e(Log.ERROR, "ShellUtils", "Rejected command due to illegal characters: " + cmd, null); + return false; + } + + // 检查多命令(逻辑运算符限制) + if (cmd.contains("&&") || cmd.contains("||")) { + Log.d("ShellUtils", "Command contains logical operators."); + if (!isExpectedMultiCommand(cmd)) { + LogUtils.e(Log.ERROR, "ShellUtils", "Rejected command due to prohibited structure: " + cmd, null); + return false; + } + } + + // 路径遍历保护 + if (cmd.contains("../") || cmd.contains("..\\")) { + LogUtils.e(Log.ERROR, "ShellUtils", "Rejected command due to path traversal attempt: " + cmd, null); + return false; + } + + // 命令长度限制 + if (cmd.startsWith("tar") && cmd.length() > 800) { // 特定命令支持更长长度 + LogUtils.e(Log.ERROR, "ShellUtils", "Command rejected due to excessive length.", null); + return false; + } else if (cmd.length() > 500) { + LogUtils.e("ShellUtils", "Command rejected due to excessive length.", null); + return false; + } + + Log.d("ShellUtils", "Command passed safety checks: " + cmd); + return true; + } + + // 附加方法:检查多命令是否符合预期 + private static boolean isExpectedMultiCommand(String cmd) { + // 判断是否为允许的命令组合,比如 `cd` 或 `tar` 组合命令 + return cmd.matches("^cd .+ && (tar|zip|cp).+"); + } + + public static List execRootCmds(List cmds) { + List results = new ArrayList<>(); + Process process = null; + try { + // 初始化 Shell 环境 + process = hasBin("su") ? Runtime.getRuntime().exec("su") : Runtime.getRuntime().exec("sh"); + + // 启动读取线程 + Process stdProcess = process; + Thread stdThread = new Thread(() -> { + try (BufferedReader stdReader = new BufferedReader(new InputStreamReader(stdProcess.getInputStream()))) { + List localResults = new ArrayList<>(); + String line; + while ((line = stdReader.readLine()) != null) { + localResults.add(line); + Log.d("ShellUtils", "Stdout: " + line); + } + synchronized (results) { + results.addAll(localResults); + } + } catch (IOException ioException) { + LogUtils.e(Log.ERROR, "ShellUtils", "Error reading stdout", ioException); + } + }); + + Process finalProcess = process; + Thread errThread = new Thread(() -> { + try (BufferedReader errReader = new BufferedReader(new InputStreamReader(finalProcess.getErrorStream()))) { + String line; + while ((line = errReader.readLine()) != null) { + LogUtils.e(Log.ERROR, "ShellUtils", "Stderr: " + line, null); + } + } catch (IOException ioException) { + LogUtils.e(Log.ERROR, "ShellUtils", "Error reading stderr", ioException); + } + }); + + // 启动子线程 + stdThread.start(); + errThread.start(); + + try (OutputStream os = process.getOutputStream()) { + for (String cmd : cmds) { +// if (!isCommandSafe(cmd)) { +// Log.w("ShellUtils", "Skipping unsafe command: " + cmd); +// continue; +// } + os.write((cmd + "\n").getBytes()); + Log.d("ShellUtils", "Executing command: " + cmd); + } + os.write("exit\n".getBytes()); + os.flush(); + } + + try { + // 执行命令、等待解决 + process.waitFor(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); // 恢复中断 + LogUtils.e(Log.ERROR, "ShellUtils", "Error executing commands", e); + } + + // 等待子线程完成 + stdThread.join(); + errThread.join(); + + } catch (InterruptedIOException e) { + LogUtils.e(Log.ERROR, "ShellUtils", "Error reading stdout: Interrupted", e); + Thread.currentThread().interrupt(); // 恢复线程的中断状态 + } catch (Exception e) { + LogUtils.e(Log.ERROR, "ShellUtils", "Error executing commands", e); + } finally { + if (process != null) { + process.destroy(); + } + } + return results; + } + + public static boolean hasRootAccess() { + // 记录是否出现安全异常 + boolean hasSecurityError = false; + + // 检查二进制文件 + String[] binariesToCheck = {"su", "xu", "vu"}; + for (String bin : binariesToCheck) { + try { + if (hasBin(bin)) { + return true; + } + } catch (SecurityException e) { + hasSecurityError = true; + LogUtils.e(Log.ERROR, "ShellUtils", "Security exception while checking: " + bin, e); + } + } + + // 判断如果发生安全异常则反馈问题 + if (hasSecurityError) { + Log.w("ShellUtils", "Potential security error detected while checking root access."); + } + + // 没有找到合法的二进制文件,则认为无root权限 + return false; + } +} diff --git a/app/src/main/java/com/android/grape/util/StoragePermissionHelper.kt b/app/src/main/java/com/android/grape/util/StoragePermissionHelper.kt new file mode 100644 index 0000000..58278ba --- /dev/null +++ b/app/src/main/java/com/android/grape/util/StoragePermissionHelper.kt @@ -0,0 +1,248 @@ +package com.android.grape.util + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat + +object StoragePermissionHelper { + + // 权限请求代码常量 + private const val REQUEST_CODE_BASIC_STORAGE_PERMISSION = 1001 + private const val REQUEST_CODE_MANAGE_ALL_FILES_PERMISSION = 1002 + + /** + * 检查是否已授予所有必要权限 + */ + fun hasFullStoragePermission(context: Context): Boolean { + return when { + // Android 10 及以下 + Build.VERSION.SDK_INT < Build.VERSION_CODES.R -> + hasBasicStoragePermission(context) + + // Android 11+ 管理所有文件权限 + else -> Environment.isExternalStorageManager() + } + } + + /** + * 请求文件存储权限(兼容所有版本) + * + * @param activity 当前活动上下文 + * @param onGranted 所有权限已授予的回调 + * @param onDenied 权限被拒绝的回调 + */ + fun requestFullStoragePermission(activity: Activity, onGranted: () -> Unit, onDenied: () -> Unit) { + // 检查是否已有权限 + if (hasFullStoragePermission(activity)) { + onGranted() + return + } + + when { + // Android 11+ - 请求管理所有文件权限 + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> + requestManageExternalStorage(activity, onGranted, onDenied) + + // Android 10 - 只需要读取权限 + Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> + requestBasicPermissions(activity, + permissions = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + onGranted = onGranted, + onDenied = onDenied + ) + + // Android 6-9 - 需要读写权限 + else -> + requestBasicPermissions(activity, + permissions = arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ), + onGranted = onGranted, + onDenied = onDenied + ) + } + } + + /** + * 处理权限请求结果 + * + * @param activity 当前活动 + * @param requestCode 请求代码 + * @param grantResults 授权结果数组 + * @param onGranted 所有权限已授予的回调 + * @param onDenied 权限被拒绝的回调 + */ + fun handlePermissionResult( + activity: Activity, + requestCode: Int, + grantResults: IntArray, + onGranted: () -> Unit, + onDenied: () -> Unit + ) { + when (requestCode) { + REQUEST_CODE_BASIC_STORAGE_PERMISSION -> { + handleBasicPermissionResult(grantResults, onGranted, onDenied) + } + // 管理权限结果在 onActivityResult 中处理 + } + } + + /** + * 处理从设置返回的结果(用于 MANAGE_EXTERNAL_STORAGE) + * + * @param activity 当前活动 + * @param requestCode 请求代码 + * @param onGranted 权限已授予的回调 + * @param onDenied 权限被拒绝的回调 + */ + @RequiresApi(Build.VERSION_CODES.R) + fun handleActivityResult( + activity: Activity, + requestCode: Int, + onGranted: () -> Unit, + onDenied: () -> Unit + ) { + if (requestCode == REQUEST_CODE_MANAGE_ALL_FILES_PERMISSION) { + if (Environment.isExternalStorageManager()) { + onGranted() + } else { + onDenied() + } + } + } + + /** + * 显示权限解释对话框 + */ + fun showPermissionRationale( + activity: Activity, + message: String = "需要文件访问权限来执行此操作", + onContinue: () -> Unit + ) { + AlertDialog.Builder(activity) + .setTitle("权限需要") + .setMessage(message) + .setPositiveButton("继续") { _, _ -> onContinue() } + .setNegativeButton("取消", null) + .show() + } + + /** + * 打开应用设置页面 + */ + fun openAppSettings(activity: Activity) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.parse("package:${activity.packageName}") + } + activity.startActivity(intent) + } + + // 私有方法 ------------------------------------------------------------ + + private fun hasBasicStoragePermission(context: Context): Boolean { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> + ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED + + else -> + ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + context, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED + } + } + + private fun requestBasicPermissions( + activity: Activity, + permissions: Array, + onGranted: () -> Unit, + onDenied: () -> Unit + ) { + if (permissions.all { + ActivityCompat.shouldShowRequestPermissionRationale(activity, it) + }) { + // 显示解释 + showPermissionRationale(activity, "这些权限对于应用功能是必需的") { + ActivityCompat.requestPermissions( + activity, + permissions, + REQUEST_CODE_BASIC_STORAGE_PERMISSION + ) + } + } else { + // 直接请求 + ActivityCompat.requestPermissions( + activity, + permissions, + REQUEST_CODE_BASIC_STORAGE_PERMISSION + ) + } + } + + private fun handleBasicPermissionResult( + grantResults: IntArray, + onGranted: () -> Unit, + onDenied: () -> Unit + ) { + val allGranted = grantResults.isNotEmpty() && + grantResults.all { it == PackageManager.PERMISSION_GRANTED } + + if (allGranted) { + onGranted() + } else { + onDenied() + } + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun requestManageExternalStorage( + activity: Activity, + onGranted: () -> Unit, + onDenied: () -> Unit + ) { + // 检查是否已经有权限 + if (Environment.isExternalStorageManager()) { + onGranted() + return + } + + try { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply { + data = Uri.parse("package:${activity.packageName}") + } + activity.startActivityForResult(intent, REQUEST_CODE_MANAGE_ALL_FILES_PERMISSION) + } catch (e: Exception) { + // 处理不支持 ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION 的设备 + try { + val fallbackIntent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION).apply { + data = Uri.parse("package:${activity.packageName}") + } + activity.startActivityForResult(fallbackIntent, REQUEST_CODE_MANAGE_ALL_FILES_PERMISSION) + } catch (ex: Exception) { + // 最终回退到应用设置页面 + showPermissionRationale(activity, "请授予管理所有文件权限") { + openAppSettings(activity) + onDenied() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/Util.kt b/app/src/main/java/com/android/grape/util/Util.kt new file mode 100644 index 0000000..e9b0787 --- /dev/null +++ b/app/src/main/java/com/android/grape/util/Util.kt @@ -0,0 +1,3196 @@ +package com.android.grape.util + +import android.app.ActivityManager +import android.app.AppOpsManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.res.AssetManager +import android.net.Uri +import android.os.Binder +import android.os.Build +import android.os.Environment +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.util.Log +import com.android.grape.MainApplication +import com.android.grape.job.AutoJobService +import com.android.grape.job.DownloadAppJobService +import com.android.grape.job.MonitorService +import com.android.grape.job.UnInstallService +import com.android.grape.net.HttpUtils +import com.android.grape.net.MyPost +import com.android.grape.sai.ApkSourceBuilder +import com.android.grape.sai.FlexSaiPackageInstaller +import com.android.grape.sai.RootedSaiPackageInstaller +import com.android.grape.sai.inter.ApkSource +import com.android.grape.sai.param.SaiPiSessionParams +import com.android.grape.sai.prefers.PreferencesHelper +import com.blankj.utilcode.util.ActivityUtils +import com.blankj.utilcode.util.LogUtils +import org.json.JSONObject +import java.io.BufferedReader +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.FileWriter +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.io.LineNumberReader +import java.io.PrintWriter +import java.io.UnsupportedEncodingException +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLDecoder +import java.util.Locale +import java.util.Properties +import java.util.Random +import kotlin.math.min + +/** + * `util`类用作封装多种方法和属性的实用程序类 + * 用于管理与应用程序相关的数据并执行各种操作。此课程提供 + * 诸如检索和更新应用程序元数据,管理日志,处理等功能 + * 备份要求,跟踪用户和组信息以及与shell命令进行交互。 + * + * 它还揭示了使用JSON数据,时间戳,代理配置,软件包的方法 + * 所有权和其他特定于应用程序的属性。该课程中的许多方法都相关 + * 将应用程序状态管理和设备上的特定命令执行。 + * + * 注意:此类包括用于初始化和内部配置的方法 + * 不直接暴露于外部使用。 + */ +object Util { + private var nRandom = 0 + private const val TAG = "IOSTQ:Util" + private const val tag = "3" + private const val sendRefer = true + var isNeedRestored: Boolean = false + const val AUTO_PACKAGENAME: String = "com.play4u.luabox" + const val AUTO_JSPACKAGENAME: String = "org.autojs.autojs" + const val proxy_packagename: String = "com.tunnelworkshop.postern" + const val AUTO_CLASSNAME: String = "com.cyjh.elfin.activity.SplashActivity" + private const val hookPackageName = "com.affsystem.androidhooker" + private const val hookAppMainClass = "com.affsystem.androidhooker.MainActivity" + private const val monitorDir = "monitor" + private const val apkDir = "apks" + private const val monitorFile = "monitor.txt" + private const val taskFile = "task.txt" + private var defaultAppJo = JSONObject() + private var defaultPRoxyJo = JSONObject() + val country: String? = null + var reloginRecordId: Long = 0L + private var appVer: String? = null + private var appVerCode: Long? = null + + /** + * 任务json + */ + var taskJson: JSONObject? = null + private set + var paramsJson: JSONObject? = null + private set + private val cacheJson: JSONObject? = null + var recordPackageName: String? = null + var recordFileName: String? = null + private var recordExtraFileName: String? = null + var trackingLink: String? = null + var recordId: Long = 0L + var proxyIp: String? = null + var proxyPort: Int = 0 + var proxyCountry: String? = null + var lang: String? = null + var ua: String? = null + var clickTime: Long = 0L + var installTime: Long = 0L + var installServerTimeFromGP: Long = 0L + var clickServerTimeFromGP: Long = 0L + var lastUpdateTime: Long = 0L + var instalTimeFromGp: Long = 0L + private var startInstallTime = 0L + var videoProxy: String = "" + var forwardIp: String? = "" + private var referer: String? = null + private var appDataUrl = "" + private var afLog = "" + var afApp: String = "" + var preClickRecordId: Long = 0 + var backFileName: String? = null + private set + var backFileName1: String? = null + private set + var backFileName2: String? = null + private set + + private var installRet: Boolean? = null + var isClickRet: Boolean = false + var clickErrReason: String = "" + private var appAfVer = "" + var delegateIp: String? = null + var isNeedReg: Boolean = false + var isNeedBackup: Boolean = false + var regEmailJson: JSONObject? = null + var isCanAuto: Boolean = false + var canAutoLc: String = "" + var canAutoAtc: String = "" + const val TYPE_SUCCESS: Int = 0 + const val TYPE_FAILED: Int = 1 + const val TYPE_PAUSED: Int = 2 + const val TYPE_CANCELED: Int = 3 + + var backupResult: JSONObject? = null + var backUpServerIp: String = "" + var isCanceled: Boolean = false + var isPaused: Boolean = false + var ctit: Int = 0 + private set + var keepOpen: Int = 0 + private var tags: String? = null + private const val isCollectURL = false + var fuzzy_domain: String = "" + var fuzzy_proxy: String? = "" + var script_status: Int = 0 + private const val testForSetInfo = false + var logBuffer: StringBuffer = StringBuffer() + private set + var scriptOpenApp: Int = 0 + private var mainUserAndGroup: String? = null + + private const val apk_path = "/sdcard/Android/data/sperixlabs.proxyme/files/monitor/apks" + + var script_path: String = "/sdcard/apks/script.zip" + + private var name: String? = null + private var baoming: String? = null + private var zip_name: String? = null + private var oldpath: String? = null + private var newpath: String? = null + + init { + initDefaultAppJo() + initDefaultProxyJo() + } + + /** 检索与应用程序关联的主要用户和组信息。 + * 如果该信息尚未缓存,则将计算并存储以供将来访问。 + * + * @param上下文用于检索软件包信息和文件路径的应用程序上下文。 + * @return代表与应用程序关联的主要用户和组的字符串。 + */ + fun getMainUserAndGroup(context: Context): String? { + if (mainUserAndGroup == null) { + mainUserAndGroup = getUserAndGroupSh( + context, + context.packageName, + getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + context.packageName + ".txt" + ) + } + + Log.i(TAG, "mainUserAndGroup:" + mainUserAndGroup) + + return mainUserAndGroup + } + + fun addLog(log: String?) { + logBuffer.append(log).append("\r\n") + } + + + val emailStr: String + /** + * 根据电子邮件类型和关联的详细信息生成格式的电子邮件字符串。 + * 该方法处理JSON对象“ Regemailjson”来确定电子邮件的类型 + * 并相应地构造字符串。 + * 如果电子邮件类型为“ gmail”,则其他详细信息,例如国家,排序订单, + * 将电子邮件地址,密码和帮助邮件附加到字符串上。 + * 如果电子邮件类型为“正常”,则只附加了国家和排序订单。 + * 如果例外,该方法返回默认值“ normal”。 + * + * @return一个代表电子邮件详细信息的字符串,根据电子邮件类型格式化 + * 或发生错误时“正常”。 + */ + get() { + try { + val emailType = + regEmailJson!!.getString("type") + val sb = StringBuffer() + sb.append(emailType) + if ("gmail".equals(emailType, ignoreCase = true)) { + sb.append("|") + .append(regEmailJson!!.getString("country")) + .append("|") + sb.append(regEmailJson!!.getInt("sort")).append("|") + sb.append(regEmailJson!!.getString("gmail")) + .append("|") + sb.append(regEmailJson!!.getString("passwd")) + .append("|") + sb.append(regEmailJson!!.getString("helpMail")) + } else if ("normal".equals(emailType, ignoreCase = true)) { + sb.append("|") + .append(regEmailJson!!.getString("country")) + .append("|") + sb.append(regEmailJson!!.getInt("sort")) + } + return sb.toString() + } catch (e: Exception) { + e.printStackTrace() + } + + return "normal" + } + + fun getAppAfVer(): String { + return appAfVer + } + + fun setAppAfVer(appAfVerV: String) { + Log.i(TAG, "setAppAfVer: $appAfVerV") + appAfVer = appAfVerV + } + + var appVersion: String? + get() = appVer + set(appAfVerV) { + Log.i(TAG, "setAppAfV: $appAfVerV") + appVer = appAfVerV + } + + var appVersionCode: Long? + get() = appVerCode + set(appVerCode) { + Log.i(TAG, "setAppAfVerCode: $appVerCode") + Util.appVerCode = appVerCode + } + + + fun setInstallRet(installRetV: Boolean) { + Log.i(TAG, "setInstallRet: $installRetV") + installRet = installRetV + } + + fun isInstallRet(): Boolean { + if (installRet == null) { + return false + } + return installRet!! + } + + fun setAfLog(afLogV: String) { + afLog = afLogV + } + + fun getAfLog(): String { + //return Util.afLog; + return ServiceUtils.readFile("/data/system/Logs.txt") + } + + + fun setReferer(refererV: String) { + Log.i(TAG, "setReferer: $refererV") + referer = refererV + } + + fun getReferer(): String? { + return referer + } + + fun setRecordExtraFileName(extraFileNameV: String?) { + recordExtraFileName = extraFileNameV + } + + fun HkLog(): String { + return "" + } + + fun HkVer(): String { + return "v1060" + } + + private fun init() { + scriptOpenApp = 0 + fuzzy_proxy = null + backupResult = null + backUpServerIp = "" + fuzzy_domain = "" + referer = "" + isNeedBackup = false + isNeedRestored = false + taskJson = null + installRet = false + paramsJson = null + recordPackageName = null + recordFileName = null + recordExtraFileName = null + trackingLink = null + proxyIp = null + proxyPort = 0 + proxyCountry = null + lang = null + delegateIp = null + ua = null + recordId = 0L + videoProxy = "" + forwardIp = null + referer = null + appDataUrl = "" + installTime = 0L + instalTimeFromGp = 0L + startInstallTime = 0L + clickTime = 0L + installTime = 0L + afLog = "" + + backFileName = null + backFileName1 = null + backFileName2 = null + + installRet = null + isClickRet = false + clickErrReason = "" + appAfVer = "" + afApp = "" + + ctit = 0 + keepOpen = 0 + + isNeedReg = false + isNeedBackup = false + regEmailJson = null + isCanAuto = false + canAutoAtc = "" + canAutoLc = "" + + logBuffer = StringBuffer() + } + + private fun initDefaultAppJo() { + try { + defaultAppJo = JSONObject( + "{\"packname\":\"luxury.best.mycamerafilter\",\"minsdk\":19,\"appname\":\"Super Fast Filter\",\"sig_sha256\":\"a2a087582d4b08db38bb50a5a4a7e588513688d81c044e5ae6aa7569dd18c454\",\"vercode\":7,\"apkmd5\":\"81d97721c197506795bd4697da916edc\",\"sig_sha1\":\"80cd0ca6eb908870d89f2ee7b9ff06d080512562\",\"sig\":\"MIIDSzCCAjOgAwIBAgIEKbAxNjANBgkqhkiG9w0BAQsFADBVMQswCQYDVQQGEwJLUjEMMAoGA1UECBMDUHJvMQ0wCwYDVQQHEwRDaXR5MQwwCgYDVQQKEwNPcmcxCzAJBgNVBAsTAk9VMQ4wDAYDVQQDEwVZb3VuZzAgFw0yMDEyMzExNTE5MTNaGA8yMDc1MTAwNDE1MTkxM1owVTELMAkGA1UEBhMCS1IxDDAKBgNVBAgTA1BybzENMAsGA1UEBxMEQ2l0eTEMMAoGA1UEChMDT3JnMQswCQYDVQQLEwJPVTEOMAwGA1UEAxMFWW91bmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCnkvDWPAkmp0RriiRp0pdKWvG3EvYP+EQ3cKUBz2IEygZRK4rH6dfHnzG0AbqZE/eTV1ISiA8sOotlCIAukq3DdBpg+2WFmO7RO//0lhxc2L+QJJsSyrweeE7jCgyTTRCTf/NUqNhCsscGaFMa2JYU/FVDQDDGNXS1rPk6Tx+D+B+XWu7nZBioH0Bcp6rEJ+FD23+tHKEwXiw7znKUojuJ+wtZjWqFNI6L0PkI5rI7Ckg/NwBqGIT9xEL8TDZATdIIOb4c3QbqhEdxA42/PFnXTmWG8W3AcYKivvdRCXmFPhz++e4NRxZHnbQGvRfbl0yKs8U2ViHPV9YMooIaVMAnAgMBAAGjITAfMB0GA1UdDgQWBBQ5Bpf8zg0LB3RTfMr7tKo+e7+RujANBgkqhkiG9w0BAQsFAAOCAQEAl/JZliWRvfKGP46L4vF3PYvvg+61Iho6giPq+zYLmFFhtw67Vc5bUrBqn5t+rAZ5EO871pqnB326SJW+Q0Iy2W/z05MK5QS302R8RYB+9DyQQHCi9c7NVsYoNoEQ3BCh/K01qtYfkE+xoKSMsiWhTtrpoNIkxh2pVjxAcos0MZcOjN0htP6xkXL24uEMSZTkmfv3Wyty1OFvdguncRJJksPVb+Xwt3gCOzsFbuPtG06NlwfN3R4PhfaeIapil5zMhbzWnx7OPoNXYYJhk+rAiVI8wPcQEo4+MGXEyCKma6OZzPFSNoRZBOdEsFAe21/D/0heRHJ2BveHwtA6jcx6zg==\",\"vername\":\"1.0.4\",\"apksize\":3609050,\"sig_md5\":\"6c6e8954878832e065f2a12bf21aa40c\",\"appid\":\"5133078\",\"slotid\":\"945726662\",\"adType\":1}" + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun initDefaultProxyJo() { + try { + defaultPRoxyJo = JSONObject("{\"proxyPort\": 0,\"proxyIp\": \"\"}") + } catch (e: Exception) { + } + } + + /** + * 递归更改指定目录及其内容的所有权 + * 使用Shell命令给提供的用户和组。 + * + * @param上下文调用该方法的上下文 + * @param dir是将更改所有权的目录的路径 + * @param groupanduser组和用户以“组:用户”的形式 + * @return true如果操作成功,则否则为否则 + */ + fun chownSh(context: Context?, dir: String?, groupAndUser: String?): Boolean { + try { + val cmd = "chown -R $groupAndUser $dir" + Log.i(TAG, "chownSh cmd:$cmd") + MockTools.exec(cmd) + return true + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + + /** + * 检索设备上与特定软件包名称关联的用户ID。 + * + * @param 上下文用于访问PackageManager的应用程序上下文。 + * @param packagename要检索用户ID的软件包的名称。 + * @return格式化的用户ID作为字符串,或者如果发生错误,则为空字符串。 + */ + fun getPackageUserID(context: Context, packageName: String): String { + var userID = "" + try { + val pm: PackageManager = context.packageManager + val ai: ApplicationInfo = pm.getApplicationInfo(packageName, 0) + val id = ai.uid + val sID = "u0_a" + (id % 10000) + userID = "$sID:$sID" + } catch (e: Exception) { + e.printStackTrace() + } + return userID + } + + + /** + * 检查应用程序是否需要通过比较当前安装的版本进行升级 + * 带有录制版本。 + * + * @param上下文应用程序访问软件包管理器以获取的上下文 + * 包装信息。 + * @return a布尔值,指示是否需要更新。如果版本代码返回true + * 不匹配或找不到软件包,否则为错误。 + */ + //判断版本是否更新 + fun CheckAppNeedUpgrade(context: Context): Boolean { + val pckMan: PackageManager = context.packageManager + val items = ArrayList>() + val packageInfo: List = pckMan.getInstalledPackages(0) + + for (pInfo in packageInfo) { + if (pInfo.packageName == recordPackageName) { + return if (appVersionCode != pInfo.versionCode.toLong()) { + true + } else { + false + } + } + } + return true + } + + /** + * 执行shell命令以列出目录的内容,重定向输出 + * 到指定的文件,并将文件所有权修改给特定用户。 + * + * @param上下文用于检索文件路径和用户信息的应用程序上下文。 + * @param dir需要列出其内容的目录。 + * @return `true`如果操作成功而没有异常,`false`否则。 + */ + fun listSh(context: Context, dir: String): Boolean { + Log.i(TAG, "listSh->dir:$dir") + try { + val txtFileName = getRecordListTxtFileName(context) + val cmd = "ls $dir -l > $txtFileName" + MockTools.exec(cmd) + val uid = getPackageUserID( + context, + recordPackageName!! + ) + chownSh(context, getMonitorDir(context), uid) + return true + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + + + fun getCatFileName(context: Context): String { + return getBaseFilesDir(context) + "/" + monitorDir + "/catSh.txt" + } + + fun getMonitorDir(context: Context): String { + return getBaseFilesDir(context) + "/" + monitorDir + "/" + } + + + /** + * 执行shell命令以检索指定文件的内容。 + * + * @param上下文应用程序上下文 + * @param fileName要读取的文件的名称 + * @return文件的内容作为字符串或一个空字符串,如果发生异常 + */ + fun catSh(context: Context?, fileName: String): String { + Log.i(TAG, "catSh->fileName:$fileName") + try { + val cmd = "cat $fileName" + val result = MockTools.execRead(cmd) + return result + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } + + /** + * 获得某个安装包的安装用户 + * + * @param context + * @return + */ + fun getUserAndGroupSh(context: Context): String { + return getUserAndGroupSh( + context, + recordPackageName, getRecordTxtFileName(context) + ) + } + + fun getSelfUserAndGroupSh(context: Context): String { + return getSelfUserAndGroupSh( + context, + context.packageName, + getSelfRecordTxtFileName(context) + ) + } + + /** + * 在应用程序上下文中检索特定软件包名称的用户和组信息。 + * 此方法利用shell命令执行查找,并将用户和组返回单个字符串。 + * + * @param上下文应用程序上下文 + * @param pkgname包装名称以找到用户和组 + * @param txtfileName文件的名称以写入输出内容或日志相关详细信息 + * @return格式中的字符串“用户:组”(如果成功)或一个空字符串(如果没有找到数据或发生错误) + */ + fun getSelfUserAndGroupSh(context: Context?, pkgName: String, txtFileName: String): String { + var PrintWriter: PrintWriter? = null + var process: Process? = null + try { +// String txtFileName = getRecordTxtFileName(context); + + process = Runtime.getRuntime().exec("su") + PrintWriter = PrintWriter(process.outputStream) + val cmd = "ls /data/data -l | grep $pkgName" + PrintWriter.println(cmd) + PrintWriter.flush() + Log.i(TAG, "getUserAndGroupSh cmd:$cmd") + PrintWriter.close() + + val ir = InputStreamReader(process.inputStream) + val input = LineNumberReader(ir) + var lines: String + val sb = StringBuffer() + process.waitFor() + while ((input.readLine().also { lines = it }) != null) { + sb.append(lines + "\n") + } + println("sb:$sb") + + + val txtFile = File(txtFileName) + + if (sb.length > 20) { + val contents = sb.toString() + Log.i( + TAG, + "getUserAndGroupSh file->$txtFileName; contents:$contents" + ) + if (contents != null && contents.length > 0) { + val arr = + contents.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + var userAndGroup = "" + for (i in arr.indices) { + val line = arr[i] + Log.i( + TAG, + "getUserAndGroup: line=$line" + ) + + if (line.endsWith(" $pkgName")) { + val arr1 = line.split(" ".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + for (s1 in arr1) { + if (s1.startsWith("u0_")) { + val user = s1 + userAndGroup = "$user:$user" + Log.i( + TAG, + "getUserAndGroupSh userAndGroup:$userAndGroup" + ) + break + } + } + } + } + return userAndGroup + } + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + process?.destroy() + } + + return "" + } + + /** + * 检索指定软件包的用户和组信息。 + * + * 此方法以特定方式执行命令列出目录内容并解析结果 + * 提取与软件包名称关联的用户和组信息。 + * + * @param上下文应用程序上下文 + * @param pkgname检索用户和组信息的软件包名称 + * @param txtfileName可以记录或处理结果的文件名 + * @return一个代表用户和组的字符串以“用户:group”为单位,如果成功, + * 或一个空字符串,如果无法检索信息 + */ + fun getUserAndGroupSh(context: Context?, pkgName: String?, txtFileName: String): String { + try { +// String txtFileName = getRecordTxtFileName(context); + + val cmd = "ls /data/data -l | grep $pkgName" + val result = MockTools.execRead(cmd) + + val txtFile = File(txtFileName) + + if (result.length > 20) { + val contents = result + Log.i( + TAG, + "getUserAndGroupSh file->$txtFileName; contents:$contents" + ) + if (contents != null && contents.length > 0) { + val arr = + contents.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + var userAndGroup = "" + for (i in arr.indices) { + val line = arr[i] + Log.i( + TAG, + "getUserAndGroup: line=$line" + ) + + if (line.endsWith(" $pkgName")) { + val arr1 = line.split(" ".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + for (s1 in arr1) { + if (s1.startsWith("u0_")) { + val user = s1 + userAndGroup = "$user:$user" + Log.i( + TAG, + "getUserAndGroupSh userAndGroup:$userAndGroup" + ) + break + } + } + } + } + return userAndGroup + } + } + } catch (e: Exception) { + e.printStackTrace() + } + + return "" + } + + + /** + * 通过将请求发送到具有必要参数的指定URL来执行Relogin任务。 + * 此方法初始化必要组件,构造请求参数,提出邮政请求, + * 并处理响应以确定下一步。 + * + * @param上下文呼叫组件的上下文,用于检索资源和系统级服务 + */ + fun execReloginTask(context: Context) { + init() + // adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context); + val url = "http://39.103.73.250/tt/ddj/preRequest!requestRelogin.do" + // String params = "platform=Android&tag="+getTag(context)+"&uuid="+PrefUtil.getUUID(context); + val ANDROID_ID = + Settings.System.getString(context.contentResolver, Settings.Secure.ANDROID_ID) + val params = "platform=Android&tag=119&uuid=$ANDROID_ID" + val valid = false + println("IOSTQ:execReloginTask->url:$url?$params") + try { + val result: String? = MyPost.postData(" ".toByteArray(charset("utf-8")), + "$url?$params" + ) + + if (result != null && result.length > 0) { + taskJson = JSONObject(result).apply { + val code = getInt("code") + if (code == 1) { + MockTools.exec("chmod 777 /data/data/com.android.grape/files/monitor") + MockTools.exec("chmod 777 /data/data/com.android.grape/files/monitor/apks") + execSetJson(context) + clickTime = 1 + } else { + Log.i( + TAG, + "request result code invalid : $code" + ) + setFinish(context) + } + } + } else { + Log.i(TAG, "request result is null") + setFinish(context) + } + } catch (e: Exception) { + Log.i(TAG, "execTask error:" + e.message) + setFinish(context) + } + } + + /** + * 通过启动必要的配置执行安装任务, + * 将请求发送到带有某些参数的指定服务器URL, + * 并处理响应以执行后续操作。 + * + * @param 上下文执行任务的上下文。它习惯了 + * 访问系统资源和特定于应用程序的详细信息。 + */ + fun execInstallTask(context: Context) { + init() + // adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context); + val url = "http://39.103.73.250/tt/ddj/preRequest!requestInstall.do" + + // String params = "platform=Android&tag="+getTag(context)+"&uuid="+PrefUtil.getUUID(context); + val ANDROID_ID = + Settings.System.getString(context.contentResolver, Settings.System.ANDROID_ID) + + val params = "platform=Android&tag=119&uuid=$ANDROID_ID" + //+ "&id=515382023"; +// boolean valid = false; + printStr("IOSTQ:request result : $url?$params") + try { + val result: String? = MyPost.postData(" ".toByteArray(charset("utf-8")), + "$url?$params" + ) + + printStr("request result : $result") + + if (result != null && result.length > 0) { + taskJson = JSONObject(result) + + val code = taskJson!!.getInt("code") + + if (code == 1) { + MockTools.exec("chmod 777 /data/data/com.android.grape/files/monitor") + MockTools.exec("chmod 777 /data/data/com.android.grape/files/monitor/apks") + execSetJson(context) + clickTime = 1 + } else { + LogUtils.i(TAG, "request result code invalid : "+code); + setFinish(context) + } + } else { + LogUtils.i(TAG, "request result is null"); + setFinish(context) + } + } catch (e: Exception) { + LogUtils.i(TAG, "execTask error:"+e.message); + setFinish(context) + } + } + + /** + * 根据随机条件执行任务。 + * 在运行Relogin任务和安装任务之间交替。 + * + * @param上下文执行任务执行的上下文 + */ + fun execTask(context: Context) { + nRandom++ + + if (nRandom % 3 == 0) { +// execInstallTask(context); + execReloginTask(context) + } else { + execInstallTask(context) + } + } + + fun getStringFromFile(context: Context?, filepath: String): String { + Log.i( + TAG, + "start to getStringFromFile : $filepath" + ) + val file = File(filepath) + if (file.exists()) { + return catSh(context, filepath) + } + return "" + } + + fun getStringFromInputStream(inputStream: InputStream?): String { + var inputStreamReader: InputStreamReader? = null + try { + inputStreamReader = InputStreamReader(inputStream, "utf-8") + } catch (e1: UnsupportedEncodingException) { + e1.printStackTrace() + } + val reader = BufferedReader(inputStreamReader) + val sb = StringBuffer("") + var line: String? + try { + while ((reader.readLine().also { line = it }) != null) { + sb.append(line) + sb.append("\n") + } + } catch (e: IOException) { + e.printStackTrace() + } + return sb.toString() + } + + fun printStr(ss: String?) { + if (ss != null && ss.length > 0) { + val length = ss.length + + val loop = (length - 1) / 2000 + 1 + + for (i in 0.. 0 + + if (it.has("ExecScriptZipURL") && !it.isNull("ExecScriptZipURL")) { + canAutoLc = + it.getString("ExecScriptZipURL") + } + + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + + fun WriteFile(fileName: String?, content: String?) { + try { + val writer = FileWriter(fileName, false) + writer.write(content) + writer.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + + + /** + * 通过向指定的服务器URL提出邮政请求来发送注册事件。 + * + * @param上下文调用该方法的上下文,用于执行网络操作 + * @return True如果已成功处理注册事件;错误,如果操作期间发生错误 + */ + private fun sendRegEvent(context: Context): Boolean { + val url = "http://123.56.44.45/tt/ddj/reg.do?recordId=$recordId" + + Log.i(TAG, "url:$url") + + try { + val ret: String? = MyPost.postData("".toByteArray(), url) + Log.i(TAG, "ret:$ret") + + val jo = JSONObject(ret) + + if (jo.getInt("code") == 1) { + regEmailJson = jo.getJSONObject("emailInfo") + } + + return true + } catch (e: Exception) { + e.printStackTrace() + } + + return false + } + + /** + * 通过处理任务 JSON 对象并向参数 JSON 对象添加各种属性来初始化操作所需的 JSON 参数。 + * 处理设备、优惠和其他相关数据,以准备完整的 JSON 结构。 + * + * @param context 用于执行各种操作的应用程序上下文,例如在初始化期间写入文件和发送事件。 + * 例如,在初始化期间写入文件和发送事件。 + * @return 如果 JSON 参数初始化成功,则返回 true,否则返回 false + */ + private fun initParamsJson(context: Context): Boolean { + paramsJson = JSONObject().apply { + try { + taskJson?.let { + val deviceJo = it.getJSONObject("device") + val deviceParamJo: JSONObject = DeviceConvertUtil.MGConvert(deviceJo) + if (it.has("isCollectURL")) { + deviceJo.put("isCollectURL", true) + } else { + deviceJo.put("isCollectURL", false) + } + + if (it.has("regInfo")) { + val regInfo = it.getJSONObject("regInfo") + var apks = File("/sdcard/Download/GoogleAccount.txt") + if (!apks.exists()) { + forceMakeDir(File("/sdcard/Download")) + apks = File("/sdcard/Download/GoogleAccount.txt") + } + WriteFile(apks.toString(), regInfo.toString()) + sendRegEvent(context) + } + + //add 20210409 + val offerJo = it.getJSONObject("offer") + val apkprop = it.getJSONObject("apkProp") + val thirdDetect = offerJo.getString("thirdDetect") + if ("appsflyer".equals(thirdDetect, ignoreCase = true)) { + deviceParamJo.put("afVersion", offerJo.getString("thirdVer")) + } + deviceParamJo.put("installVersionFromGP", offerJo.getString("apkVer")) + + put("device", deviceParamJo) + + if (it.has("expand")) { + put("expand", it.getJSONObject("expand")) + } + put("offer", offerJo) + val proxyJo = defaultPRoxyJo + } + } catch (e: Exception) { + e.printStackTrace() + + Log.i(TAG, "initParamsJson error : " + e.message) + setFinish(context) + return false + } + } + + return true + } + + private fun execRecord(context: Context) { + Log.i(TAG, "start to execRecord") + val apks = File(getRecordDataDirName(context)) + if (!apks.exists()) { + forceMakeDir(apks) + } + DownloadAppJobService.onEvent(context) + } + + + private fun execDownScript(): Boolean { + var isDownload = true + if (isCanAuto) { + Log.i(TAG, "start to execDownScript") + val script_url = "http://39.103.73.250/tt/" + canAutoLc + isDownload = downloadFile(script_url, script_path) + if (!isDownload) { + Log.i( + TAG, + "execDownScript isDownload : $isDownload" + ) + return false + } + + unzipAPkSh(script_path, "/sdcard/autojs/") + delFileSh(script_path) + } + return isDownload + } + + + fun execDownload(context: Context): Boolean { + return execDownloadApp(context) && execDownScript() + } + + fun execDownloadApp(context: Context): Boolean { + try { + var url = "" + + url = if (videoProxy.startsWith("http://39.103.73.250")) { + videoProxy + "/ddj/" + recordFileName + } else { + // url = videoProxy + "/ddj/" + Util.getRecordPackageName() + ".apk"; + videoProxy + "/ddj/" + recordFileName + } + url = "http://39.103.73.250/tt/upload/ddj/" + recordFileName + // if(getRecordPackageName().equals("com.zhiliaoapp.musically")) { +// url = "http://192.168.30.201:5000/static/com.zhiliaoapp.musically_24.7.5.xapk"; +// } + var ret = false + ret = if (checkAppInstalled( + context, + recordPackageName + ) || !CheckAppNeedUpgrade(context) + ) { + true + } else { + // ret = downloadFile(url, Util.getRecordApkVerFileName(context)); + downloadFile(url, "/sdcard/apks/" + recordFileName) + } + + if (ret) { + Log.i(TAG, "download apk succ") + Log.i(TAG, "recordExtraFileName:" + recordExtraFileName) + if (recordExtraFileName != null && recordExtraFileName!!.length > 0) { + val extraUrl = videoProxy + "/ddj/" + recordExtraFileName + + ret = downloadFile(extraUrl, getRecordExtraFileName(context)) + } + if (isNeedRestored) { + println("IOSTQ:开始下载留存文件") + if (taskJson!!.has("BackupFileUrl1") && taskJson!!.getString("BackupFileUrl1").length > 0) { + var restored_zip = + "http://192.168.1.111/tt/" + taskJson!!.getString("BackupFileUrl1") + if (backUpServerIp != "") { + restored_zip = + "http://" + backUpServerIp + "/tt/" + taskJson!!.getString("BackupFileUrl1") + } + ret = downloadFile(restored_zip, getRecordDataFileName(context)) + } + } + } + + return ret + } catch (e: Exception) { + e.printStackTrace() + } + + return false + } + + private fun getRecordExtraFileName(context: Context): String { + return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + recordExtraFileName + } + + fun execDownloadData(context: Context): Boolean { + try { + return downloadFile(appDataUrl, getRecordDataFileName(context)) + } catch (e: Exception) { + e.printStackTrace() + } + + return false + } + + + fun genInstallTimeFromGP(ct: Long, it: Long): LongArray { + var clickServerTimeToGP: Long + var installServerTimeFromGP: Long + + val timeOff = it - ct + if (timeOff < 180000L) { + installTime = it - 1000 * (5 + Random().nextInt(10)) + instalTimeFromGp = (installTime / 1000) - (installTime % 7) - 3 + } else if (timeOff < 600000L) { + installTime = it - 1000 * (20 + Random().nextInt(40)) + instalTimeFromGp = (installTime / 1000) - (installTime % 30) - 30 + } else { +// firstInstallTime = it - 1000 * (40+new Random().nextInt(140)); + installTime = it - 1000 * (180 + Random().nextInt((timeOff * 0.3 / 1000).toInt())) + instalTimeFromGp = (installTime / 1000) - (installTime % 120) - 180 + } + + clickServerTimeToGP = (instalTimeFromGp * 1000 - clickTime) + if (clickServerTimeToGP > 0) { + val randomNum = + if (clickServerTimeToGP.toInt() / 1000 >= 20) Random().nextInt(20) else Random().nextInt( + clickServerTimeToGP.toInt() / 1000 + ) + clickServerTimeToGP = clickTime + ((1 + Random().nextInt(1 + randomNum)) * 1000) + } + + installServerTimeFromGP = (installTime - instalTimeFromGp * 1000) + if (installServerTimeFromGP > 0) { + val randomNum = + if (installServerTimeFromGP.toInt() / 1000 >= 20) Random().nextInt(20) else Random().nextInt( + installServerTimeFromGP.toInt() / 1000 + ) + installServerTimeFromGP = instalTimeFromGp + (1 + Random().nextInt(1 + randomNum)) + } + + return longArrayOf(clickServerTimeToGP, installServerTimeFromGP) + } + + + @Throws(IOException::class) + private fun writeFileToSDCard(message: String) { + // 比如可以将一个文件作为普通的文档存储,那么先获取系统默认的文档存放根目录 + val parent_path = Environment.getExternalStorageDirectory() + + // String AppPath = "/data/data/"+ Util.getRecordPackageName() + "/"; + val AppPath = "/data/data/sperixlabs.proxyme/" + val dir = File(AppPath) + if (!dir.exists()) { + dir.mkdirs() + } + val file = File(AppPath + "device.txt") + if (file.exists()) { + file.delete() + } + + try { + file.createNewFile() + } catch (e: IOException) { + e.printStackTrace() + } + val writer = FileWriter(AppPath + "device.txt", true) + + writer.write(message) + + writer.close() + + // FileOutputStream fos = new FileOutputStream(file); +// long sum = 0; +// int lenght = 0; +// fos.write(message.getBytes(StandardCharsets.UTF_8), 0, message.length()); +// fos.flush(); +// fos.close(); + // String dstPath = "/data/data/"+ Util.getRecordPackageName() + "/" + Util.getRecordPackageName()+ "_device.txt"; + // Util.copyFileSh(AppPath +"device.txt",dstPath); + + //give777(dstPath); + // CmdUtils.chmodFilePermission(dstPath); + Log.d("文件写入", "成功") + } + + + @Throws(IOException::class) + fun setRrInfo(context: Context): Boolean { + try { + clickTime = taskJson!!.getLong("clickTime") + val deviceJo = paramsJson!!.getJSONObject("device") + + if (isNeedRestored) { + if (cacheJson != null) { + installTime = + cacheJson.getLong("firstInstallTime") + lastUpdateTime = + cacheJson.getLong("lastUpdateTime") + installServerTimeFromGP = + cacheJson.getLong("installServerTimeFromGP") + clickServerTimeFromGP = + cacheJson.getLong("clickServerTimeToGP") + instalTimeFromGp = + cacheJson.getLong("installTimeFromGP") + } + } + + + deviceJo.put("firstInstallTime", installTime) + deviceJo.put("lastUpdateTime", lastUpdateTime) + deviceJo.put("installTimeFromGP", instalTimeFromGp) + val installServerTimeFromGP = installServerTimeFromGP + deviceJo.put("installServerTimeFromGP", installServerTimeFromGP) + val clickTimeToGp = (clickTime / 1000) + deviceJo.put("clickTimeToGP", clickTimeToGp) + val clickServerTimeToGP = clickServerTimeFromGP + deviceJo.put("clickServerTimeToGP", clickServerTimeToGP) + + + if (taskJson!!.has("clickData")) { + val clickdata = taskJson!!.getJSONObject("clickData") + if (clickdata.has("referer")) { + referer = clickdata.getString("referer") + } + } + + if (getReferer() != null) { + deviceJo.put("referrerFromGP", URLDecoder.decode(getReferer(), "UTF-8")) + } else { + deviceJo.put("referrerFromGP", "utm_source=google-play&utm_medium=organic") + deviceJo.put("clickServerTimeToGP", 0) + deviceJo.put("clickTimeToGP", 0) + deviceJo.put("installTimeFromGP", 0) + deviceJo.put("installServerTimeFromGP", 0) + } + + val origin_gaid: String = AdvertisingIdClient.getGoogleAdId(context)?:"" + deviceJo.put("origin_gaid", origin_gaid) + paramsJson!!.put("device", deviceJo) + val params = paramsJson.toString() + ServiceUtils.setEnableApp(recordPackageName?:"", true) + ServiceUtils.setEnableApp("org.mozilla.firefox", true) + ServiceUtils.setEnableApp("com.google.android.webview", true) + ServiceUtils.setEnableApp("com.android.chrome", true) + + ServiceUtils.writeFile("/data/system/device.txt", params) + Log.d("IOSTQ:param == ", params) + return true + // paramsJson.put("proxy", proxyJo); + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + + @Throws(IOException::class) + fun setInfo(context: Context): Boolean { + val url = "http://127.0.0.1:8090/ctl/setinfo" + + //安装时间 + try { + clickTime = taskJson!!.getLong("clickTime") + val deviceJo = paramsJson!!.getJSONObject("device") + // installTimeFromGP = (long) (clickTime / 1000) + (clickTime % 7) + 3; + installTime = System.currentTimeMillis() + Thread.sleep(2000) + Log.d("IOSTQ:installTime == ", installTime.toString() + "") + Log.d("IOSTQ:clickTime == ", clickTime.toString() + "") + val time = genInstallTimeFromGP(clickTime, installTime) + // installTimeFromGP = firstInstallTime / 1000; + installTime = System.currentTimeMillis() + lastUpdateTime = installTime + LogUtils.d("IOSTQ:lastUpdateTime ", lastUpdateTime.toString() + "") + deviceJo.put("firstInstallTime", installTime) + deviceJo.put("lastUpdateTime", lastUpdateTime) + deviceJo.put("installTimeFromGP", instalTimeFromGp) + val installServerTimeFromGP = time[1] // + (installTimeFromGP % 2) + 1; + deviceJo.put("installServerTimeFromGP", installServerTimeFromGP) + val clickTimeToGp = (clickTime / 1000) + deviceJo.put("clickTimeToGP", clickTimeToGp) + val clickServerTimeToGP = time[0] / 1000 // + (clickTimeToGp % 2) + 1; + deviceJo.put("clickServerTimeToGP", clickServerTimeToGP) + + installTime = installTime + instalTimeFromGp = + instalTimeFromGp + lastUpdateTime = lastUpdateTime + Util.installServerTimeFromGP = installServerTimeFromGP + clickServerTimeFromGP = clickServerTimeToGP + + if (taskJson!!.has("clickData")) { + val clickdata = taskJson!!.getJSONObject("clickData") + if (clickdata.has("referer") && clickdata.getString("referer").length > 10) { + referer = clickdata.getString("referer") + } + } else { + referer = "" + } + + if (referer != null && referer!!.length > 10) { + deviceJo.put("referrerFromGP", URLDecoder.decode(referer, "UTF-8")) + } else { + deviceJo.put("referrerFromGP", "utm_source=google-play&utm_medium=organic") + deviceJo.put("clickServerTimeToGP", 0) + deviceJo.put("clickTimeToGP", 0) + deviceJo.put("installTimeFromGP", 0) + deviceJo.put("installServerTimeFromGP", 0) + } + + //JSONObject proxyJo = paramsJson.getJSONObject("proxy"); + //proxyJo.put("forwardIp", Util.getDelegateIp()); + Log.d("IOSTQ", "firstInstalTime == " + installTime) + Log.d("IOSTQ", "paramsJson == " + paramsJson) + val origin_gaid: String = AdvertisingIdClient.getGoogleAdId(context)?:"" + deviceJo.put("origin_gaid", origin_gaid) + paramsJson!!.put("device", deviceJo) + val params = paramsJson.toString() + ServiceUtils.setEnableApp(recordPackageName?:"", true) + ServiceUtils.setEnableApp("org.mozilla.firefox", true) + ServiceUtils.setEnableApp("com.google.android.webview", true) + ServiceUtils.setEnableApp("com.android.chrome", true) + + ServiceUtils.writeFile("/data/system/device.txt", params) + Log.d("IOSTQ:param == ", params) + // paramsJson.put("proxy", proxyJo); + } catch (e: Exception) { + Log.d("IOSTQ", e.message!!) + e.printStackTrace() + } + + // printStr("execSetJson params:"+params); + + // writeFileToSDCard(params); + try { + // String ret = new MyPost().PostData(context, params.getBytes("utf-8"), url); + + // Log.i(TAG, "set info ret : " + ret); + } catch (e: Exception) { + e.printStackTrace() + } + return true + } + + val paramString: String + get() = paramsJson.toString() + + private fun getRecordTxtFileName(context: Context): String { + return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + recordPackageName + ".txt" + } + + private fun getSelfRecordTxtFileName(context: Context): String { + return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + context.packageName + ".txt" + } + + private fun getRecordListTxtFileName(context: Context): String { + return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + recordPackageName + ".list.txt" + } + + private fun getRecordApkFileName(context: Context): String { + return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + recordPackageName + ".apk" + } + + private fun getRecordApkVerFileName(context: Context): String { + return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + recordFileName + } + + private fun getRecordSdcardApkVerFileName(context: Context): String { + return "/sdcard/apks/" + recordFileName + } + + private fun getRecordDataFileName(context: Context): String { + return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + recordPackageName + ".zip" + } + + private fun getRecordDataDirName(context: Context): String { + return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + } + + fun getBaseFilesDir(context: Context): String { +// File dir = context.getExternalFilesDir(null); +// if(dir == null) { +// return "/sdcard/Android/data/" + context.getPackageName() + "/files"; +// }else{ +// return dir.getAbsolutePath(); +// } + return context.filesDir.absolutePath + } + + private fun notcl(context: Context) { + val url = "http://127.0.0.1:8090/ctl/notcl" + + try { + val ret: String = MyPost.postData("".toByteArray(charset("utf-8")), url)?:"" + Log.i(TAG, "notcl ret : $ret") + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun checkHook(context: Context): Boolean { + val url = "http://127.0.0.1:8090/ctl/test" + + val checked = false + + try { + val ret: String = MyPost.postData("".toByteArray(charset("utf-8")), url)?:"" + Log.i(TAG, "checkHook ret : $ret") + if ("it works!" == ret) { + return true + } + } catch (e: UnsupportedEncodingException) { + e.printStackTrace() + } + + execHookApp(context) + + try { + Thread.sleep((1000 * 10).toLong()) + } catch (e: InterruptedException) { + e.printStackTrace() + } + + return true + } + + private fun execHookApp(context: Context) { + val intent = Intent(Intent.ACTION_MAIN) + + if (intent != null) { + val cname = ComponentName(hookPackageName, hookAppMainClass) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.setComponent(cname) + context.startActivity(intent) + } + } + + fun openRecordApp(context: Context) { + if (scriptOpenApp == 0) { + execTargetApp( + context, + recordPackageName!! + ) + } + + if (!isCanAuto || canAutoLc.length <= 10) { + Handler(Looper.getMainLooper()).postDelayed(Runnable { + setTopApp(context) + //开始卸载 + UnInstallService.onEvent(context) + }, 120000) + } + } + + + fun execTargetApp(context: Context, targetPackageName: String): Boolean { + Log.i(TAG, "开始启动应用 ...$targetPackageName") + + val packageManager: PackageManager = context.packageManager + var intent:Intent? = null + intent = packageManager.getLaunchIntentForPackage(targetPackageName) + + if (intent == null) { + return false + } else { + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + return true + } + } + + + fun doScript(context: Context, targetPackageName: String): Boolean { + execTargetApp(context, targetPackageName) + try { + Thread.sleep(3000L) + } catch (e: InterruptedException) { + e.printStackTrace() + } + Handler(Looper.getMainLooper()).postDelayed(Runnable { //开始执行脚本 + AutoJobService.onEvent(context) + }, 1) + return true + } + + fun execTargetApp( + context: Context, + targetPackageName: String, + targetAppMainClass: String + ): Boolean { + Log.i( + TAG, + "start to openTargetApp ... $targetPackageName ; $targetAppMainClass" + ) + + execTargetApp(context, targetPackageName) + return true + } + + private fun forceCreteDir(file: File) { + if (!file.exists()) { + val parent = file.parentFile + + if (parent!!.exists()) { + file.mkdir() + } else { + forceCreteDir(parent) + file.mkdir() + } + } + } + + private fun forceMakeDir(file: File) { + if (!file.exists()) { + val parent = file.parentFile + + if (!parent!!.exists()) { + MockTools.exec("mkdir $parent") + forceMakeDir(parent) + // file.mkdirs(); + } else { + MockTools.exec("mkdir $file") + } + } + } + + fun writeFile1(context: Context?, dirName: String, fileName: String, contents: String) { + val fs = File("$dirName/$fileName") + forceMakeDir(fs) + var outputStream: FileOutputStream? = null + try { + outputStream = FileOutputStream(fs) + + outputStream.write(contents.toByteArray()) + outputStream.flush() + outputStream.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun writeFile(context: Context, dirName: String, fileName: String, contents: String) { + val fs = File(getBaseFilesDir(context) + "/" + dirName + "/" + fileName) + forceMakeDir(fs) + var outputStream: FileOutputStream? = null + try { + outputStream = FileOutputStream(fs) + + outputStream.write(contents.toByteArray()) + outputStream.flush() + outputStream.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun getFileModifyTime(context: Context, dirName: String, fileName: String): Long { + val fs = File(getBaseFilesDir(context) + "/" + dirName + "/" + fileName) + + return if (fs.exists()) { + fs.lastModified() + } else { + 0L + } + } + + fun getFileContent(context: Context, dirName: String, fileName: String): String { + val fs = File(getBaseFilesDir(context) + "/" + dirName + "/" + fileName) + + if (fs.exists()) { + var fis: FileInputStream? = null + try { + fis = FileInputStream(fs) + val bytes = ByteArray(1024) + //得到实际读取的长度 + var n = 0 + val sb = StringBuffer() + //循环读取 + while ((fis.read(bytes).also { n = it }) != -1) { + val s = String(bytes, 0, n) + sb.append(s) + } + + return sb.toString() + } catch (e: Exception) { + // TODO Auto-generated catch block + e.printStackTrace() + } finally { + //最后一定要关闭文件流 + try { + fis!!.close() + } catch (e: IOException) { + // TODO Auto-generated catch block + e.printStackTrace() + } + } + } + + return "" + } + + fun getFileContent(context: Context?, fileName: String): String { + val fs = File(fileName) + + if (fs.exists()) { + var fis: FileInputStream? = null + try { + fis = FileInputStream(fs) + val bytes = ByteArray(1024) + //得到实际读取的长度 + var n = 0 + val sb = StringBuffer() + //循环读取 + while ((fis.read(bytes).also { n = it }) != -1) { + val s = String(bytes, 0, n) + sb.append(s) + } + + return sb.toString() + } catch (e: Exception) { + // TODO Auto-generated catch block + e.printStackTrace() + } finally { + //最后一定要关闭文件流 + try { + fis!!.close() + } catch (e: IOException) { + // TODO Auto-generated catch block + e.printStackTrace() + } + } + } + + return "" + } + + + fun downloadFile(httpUrl: String, fileName: String): Boolean { + Log.i( + TAG, + "start to downloadFile : $httpUrl ; $fileName" + ) + val file = File(fileName) + + if (file.exists() && file.length() >= 1024 * 1024 * 2) { //文件已经存在就不下载 且 大小超过3M + return true + } + + val create_dir = file.parentFile + forceMakeDir(create_dir!!) + + val fileLength = 0L + + try { + var conn: HttpURLConnection? = null + var `is`: InputStream? = null + var fos: FileOutputStream? = null + + val url = URL(httpUrl) + + try { + conn = url.openConnection() as HttpURLConnection + `is` = conn!!.inputStream + fos = FileOutputStream(file) + val buf = ByteArray(256) + conn.connect() + if (conn.responseCode >= 400) { + Log.i(TAG, "connection timeout") + } else { + //System.out.println("文件大小:"+fileLength); + while (true) { + if (`is` != null) { + val numRead = `is`.read(buf) + if (numRead <= 0) { + return true + } else { + fos.write(buf, 0, numRead) + } + } else { + break + } + } + + if (file.length() >= 1024 * 100) { + return true + } + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + try { + conn!!.disconnect() + } catch (e: Exception) { + } + try { + fos!!.close() + } catch (e: Exception) { + } + try { + `is`!!.close() + } catch (e: Exception) { + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + + /** + * 安装次留软件 + * + * @param context + * @return + */ + fun installRecord(context: Context): Boolean { + var installRet = false + startInstallTime = System.currentTimeMillis() + if (CheckAppNeedUpgrade(context) || !checkAppInstalled( + context, + recordPackageName + ) + ) { + try { + val file = File(getRecordSdcardApkVerFileName(context)) + var extraFile: File? = null + if (recordExtraFileName != null && recordExtraFileName!!.length > 0) { + extraFile = File(getRecordExtraFileName(context)) + } + + installRet = installApk(context, file, extraFile) && checkAppInstalled( + context, + recordPackageName + ) + + if (installRet && !videoProxy.contains("123.56.44.45")) { + //检查是否有新版本 + if (!CheckAppNeedUpgrade(context)) { + file.delete() + extraFile?.delete() + } + } + } catch (e: Exception) { + e.printStackTrace() + } + Log.i(TAG, "installRet:$installRet") + } else { + installRet = true + MockTools.exec("pm clear " + recordPackageName) + } + + + return installRet + } + + fun isBackground(context: Context): Boolean { + val activityManager: ActivityManager = + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val appProcesses: List = activityManager.getRunningAppProcesses() + var isBackground = true + var processName = "empty" + for (appProcess in appProcesses) { + if (appProcess.processName == context.packageName) { + processName = appProcess.processName + isBackground = + if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED) { + true + } else if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + || appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE + ) { + false + } else { + true + } + } + } + if (isBackground) { + Log.d(TAG, "后台:$processName") + } else { + Log.d(TAG, "前台+$processName") + } + return isBackground + } + + private fun getApkDataDir(context: Context, packageName: String): String? { + var apkDataPath: String? = null + try { + apkDataPath = context.packageManager.getApplicationInfo(packageName, 0).dataDir + return apkDataPath + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + } + return null + } + + fun recoverRecordData(context: Context): Boolean { + Log.i(TAG, "start recoverRecordData") + + try { + val dataDir = getRecordDataDirName(context) + val zipFile = getRecordDataFileName(context) + val reloginDataDir = getApkDataDir( + context, + recordPackageName!! + ) + + Log.i( + TAG, + "recoverRecordData: dir=$reloginDataDir" + ) + + // ZipUtils1.upZipFile(new File(zipFile), dataDir+"/"); + unZipFileSh(zipFile, dataDir) + + val userAnGroup = getUserAndGroupSh(context) + Log.i( + TAG, + "recoverRecordData->userAndGroup:$userAnGroup" + ) + + copyFolderSh( + dataDir + "/" + recordPackageName, + File(reloginDataDir).parentFile.absolutePath + ) + + delFilesSh( + dataDir, + recordPackageName + ) + + chownSh(context, reloginDataDir, userAnGroup) + + return true + } catch (e: Exception) { + e.printStackTrace() + } + + return false + } + + fun delFilesSh(dir: String, prefix: String?) { + try { + var cmd = "cd $dir|" + cmd += "rm -rf $prefix*" + Log.i(TAG, "delFilesSh-> cmd:$cmd") + MockTools.exec(cmd) + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun unZipFileSh(zipFileName: String, dataDir: String) { + try { + var cmd = "cd $dataDir|" + cmd += "tar -xvf " + File(zipFileName).name + Log.i(TAG, "unZipFileSh-> cmd:$cmd") + MockTools.exec(cmd) + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun unzipAPkSh(zipFileName: String, dataDir: String) { + try { + val file = File(dataDir) + if (!file.exists()) { + forceCreteDir(file) + } + val cmd = "unzip -o " + "/sdcard/apks/" + File(zipFileName).name + " -d " + dataDir + val unzipResult = MockTools.execRead(cmd) + Log.i(TAG, "unZipFileSh-> cmd:$unzipResult") + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun copyFolderSh(oldPath: String, newPath: String): Boolean { + Log.i( + TAG, + "start copyFolderSh : $oldPath ; $newPath" + ) + + try { + val cmd = "cp -r -f $oldPath $newPath" + Log.i(TAG, "copyFolderSh cmd:$cmd") + MockTools.exec(cmd) + } catch (e: Exception) { + e.printStackTrace() + } + + return false + } + + fun copyFileSh(oldPath: String, newPath: String): Boolean { + Log.i( + TAG, + "start copyFileSh : $oldPath ; $newPath" + ) + try { + val cmd = "cp -r -f $oldPath $newPath" + MockTools.exec(cmd) + } catch (e: Exception) { + e.printStackTrace() + } + + return false + } + + fun installApk(context: Context, apkFile: File, extraFile: File?): Boolean { + Log.i( + TAG, + "start to install apk ... $apkFile ; $extraFile" + ) + if (extraFile == null) { + return if (apkFile.name.endsWith(".apk")) { + clientInstall(apkFile) + } else { + clientInstallOther(context, apkFile) + } + } else { + val extraFileName = extraFile.name.lowercase(Locale.getDefault()) + + return if (extraFileName.endsWith("obb.apk")) { + clientInstallObb(context, apkFile, extraFile) + } else { + clientInstallSplit(context, apkFile, extraFile) + } + } + } + + + fun installApks4Tmp(apkName: String?, context: Context?): Boolean { + var result = MockTools.execRead("pm install-create") + Log.d(TAG, "installApks4Tmp: successMsg $result") + val session = result.substring(result.indexOf("[") + 1, result.indexOf("]")) + Log.d(TAG, "installApks4Tmp: session $session") + + val file = File("/sdcard/apks/$apkName") + val files = file.listFiles() + var bool = true + var currentApkFile = 1 + for (f in files) { + val extraName = f.name.substring(f.name.lastIndexOf('.') + 1) + Log.d( + TAG, + "installApks4Tmp: extraName $extraName" + ) + if ("apk" == extraName) { + //Log.d(TAG, "installApks4Tmp: getPath " + f.getPath()); + val commond = + "pm install-write " + session + " " + currentApkFile + ".apk " + f.path + currentApkFile++ + //Log.d(TAG, "installApks4Tmp: " + commond); + result = MockTools.execRead(commond) + if (!result.contains("Success")) { + bool = false + break + } + } + } + if (bool) { + result = MockTools.execRead("pm install-commit $session") + if (!result.contains("Success")) { + bool = false + } + } else { + result = MockTools.execRead("pm install-abandon $session") + } + for (f in files) { + MockTools.execRead("rm -rf $f") + } + setInstallRet(bool) + return bool + } + + + private fun clientInstallOther(context: Context, apkFile: File): Boolean { + Log.e(TAG, "clientInstallOther: $apkFile") + + if (apkFile.toString().contains("xapk")) { + try { + unzipAPkSh(apkFile.toString(), getRecordDataDirName(context)) + installApks4Tmp(recordPackageName, context) + } catch (e: Exception) { + e.printStackTrace() + } + } else { + Log.e("clientInstallOther", "后缀是apks") + + //将文件路径转化为Uri + val uri = Uri.fromFile(apkFile) + + val mInstaller: FlexSaiPackageInstaller = FlexSaiPackageInstaller.getInstance() + val mPrefsHelper: PreferencesHelper = PreferencesHelper.getInstance(context) + val rootedSaiPackageInstaller: RootedSaiPackageInstaller = + RootedSaiPackageInstaller.getInstance(context) + + val apkSource: ApkSource = ApkSourceBuilder(context) + .fromZipContentUri(uri) + .setZipExtractionEnabled(mPrefsHelper.shouldExtractArchives()) + .setReadZipViaZipFileEnabled(mPrefsHelper.shouldUseZipFileApi()) + .setSigningEnabled(mPrefsHelper.shouldSignApks()) + .build() + + // mInstaller.enqueueSession(mInstaller.createSessionOnInstaller(mPrefsHelper.getInstaller(), new SaiPiSessionParams(apkSource))); + rootedSaiPackageInstaller.enqueueSession( + mInstaller.createSessionOnInstaller( + mPrefsHelper.installer, + SaiPiSessionParams(apkSource) + ) + ) + } + + return isInstallRet() + } + + private fun clientInstallSplit(context: Context, apkFile: File, extraFile: File): Boolean { + val PrintWriter: PrintWriter? = null + val process: Process? = null + try { + val txtFileName = getSessionTxtFileName(context) + MockTools.exec("chmod 777 $apkFile") + MockTools.exec("pm install-create > $txtFileName") + + //Success: created install session [1649302163] + val sessionStr = getStringFromFile(context, getSessionTxtFileName(context)) + val pos0 = sessionStr.indexOf("[") + val pos1 = sessionStr.indexOf("]") + val sessionId = sessionStr.substring(pos0 + 1, pos1) + Log.i(TAG, "sessionId:$sessionId") + + MockTools.exec("chmod 777 $apkFile") + MockTools.exec("pm install-write $sessionId baseapk $apkFile") + MockTools.exec("pm install-write $sessionId splitapk $extraFile") + MockTools.exec("pm install-commit $sessionId") + + return true + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + + private fun getSessionTxtFileName(context: Context): String { + return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/sessionTxt.txt" + } + + private fun clientInstallObb(context: Context, apkFile: File, extraFile: File): Boolean { + Log.i( + TAG, + "start clientInstallObb : $apkFile ; $extraFile" + ) + + try { + MockTools.exec("chmod 777 $apkFile") + MockTools.exec("pm install -r $apkFile") + installObb(context, extraFile) + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + + private fun installObb(context: Context, extraFile: File) { + val userAndGroup = getUserAndGroupSh(context) + + val destExtraFile = File( + "/sdcard/Android/obb/" + recordPackageName + "/" + extraFile.name.replace( + ".obb.apk", + ".obb" + ) + ) + forceMakeDir(destExtraFile) + + var fileInputStream: FileInputStream? = null + var fileOutputStream: FileOutputStream? = null + + try { + fileInputStream = FileInputStream(extraFile) + fileOutputStream = FileOutputStream(destExtraFile) + val buffer = ByteArray(1024) + var byteRead: Int + while ((fileInputStream.read(buffer).also { byteRead = it }) != -1) { + fileOutputStream.write(buffer, 0, byteRead) + } + + chownSh(context, "/sdcard/Android/obb/" + recordPackageName + "/", userAndGroup) + } catch (e: Exception) { + if (fileInputStream != null) { + try { + fileInputStream.close() + } catch (e1: Exception) { + e1.printStackTrace() + } + } + + if (fileOutputStream != null) { + try { + fileOutputStream.flush() + fileOutputStream.close() + } catch (e2: Exception) { + e2.printStackTrace() + } + } + } + } + + /* + * m命令可以通过adb在shell中执行,同样,我们可以通过代码来执行 + */ + fun execCommand(vararg command: String?): String? { + var process: Process? = null + var errIs: InputStream? = null + var inIs: InputStream? = null + var result: String? = "" + + try { + process = ProcessBuilder().command(*command).start() + + val baos = ByteArrayOutputStream() + var read = -1 + errIs = process.errorStream + while ((errIs.read().also { read = it }) != -1) { + baos.write(read) + } + + inIs = process.inputStream + while ((inIs.read().also { read = it }) != -1) { + baos.write(read) + } + result = String(baos.toByteArray()) + + inIs?.close() + errIs?.close() + process.destroy() + } catch (e: IOException) { + result = e.message + } + + return result + } + + private fun give777(path: String): Boolean { + execCommand("su") + execCommand("chmod", "777", path) + return true + } + + private fun clientInstall0(apkFile: File): Boolean { + execCommand("su") + execCommand("chmod", "777", apkFile.absolutePath) + execCommand("export", "LD_LIBRARY_PATH=/vendor/lib:/system/lib") + execCommand("pm", "install", "-r", apkFile.absolutePath) + + return true + } + + private fun clientInstall(apkFile: File): Boolean { + Log.i(TAG, "clientInstall : $apkFile") + try { + MockTools.exec("chmod 777 $apkFile") + MockTools.exec("pm install -r $apkFile") + return true + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + + private fun hasRootPerssion(): Boolean { + var PrintWriter: PrintWriter? = null + var process: Process? = null + try { + process = Runtime.getRuntime().exec("su") + PrintWriter = PrintWriter(process.outputStream) + PrintWriter.flush() + PrintWriter.close() + val value = process.waitFor() + return returnResult(value) + } catch (e: Exception) { + e.printStackTrace() + } finally { + process?.destroy() + } + return false + } + + private fun returnResult(value: Int): Boolean { + // 代表成功 + return if (value == 0) { + true + } else if (value == 1) { // 失败 + false + } else { // 未知情况 + false + } + } + + fun hookOpenApp(context: Context) { + Log.i(TAG, "start to hookOpenApp : ") + + val url = "http://127.0.0.1:8090/ctl/setTarget?target=" + recordPackageName + + val ret: String = MyPost.postData("".toByteArray(), url)?:"" + + Log.i(TAG, "Hook Open App ret : $ret") + } + + /** + * 卸载 + * + * @return + */ + fun clientUninstallRecord(context: Context): Boolean { + while (recordAppInstalled(context)) { + clientUninstall(recordPackageName) + } + return true + } + + /** + * 静默卸载 + */ + fun clientUninstall(packageName: String?): Boolean { + Log.i( + TAG, + "start to clientUninstall : $packageName" + ) + try { +// String cmd = "LD_LIBRARY_PATH=/vendor/lib:/system/lib" + "|"; + val cmd = "pm uninstall $packageName" + MockTools.exec(cmd) + return true + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + + private fun recordAppInstalled(context: Context): Boolean { + return checkAppInstalled( + context, + recordPackageName + ) + } + + private fun checkAppInstalled(context: Context, pkgName: String?): Boolean { + if (pkgName == null || pkgName.isEmpty()) { + return false + } + var packageInfo = try { + context.packageManager.getPackageInfo(pkgName, 0) + } catch (e: PackageManager.NameNotFoundException) { + null + // e.printStackTrace(); + } + var exists = false + exists = if (packageInfo == null) { + false + } else { + true //true为安装了,false为未安装 + } + + Log.i(TAG, "checkAppInstalled($pkgName) : $exists") + + return exists + } + + fun setFinish(context: Context) { + // Log.i(TAG, "setFinish"); + + MonitorService.setRunning(false) + killRecordProcess(context, "com.tunnelworkshop.postern") + killRecordProcess(context, AUTO_JSPACKAGENAME) + Handler(Looper.getMainLooper()).postDelayed(Runnable { + MonitorService.onEvent( + context + ) + }, 5000) + } + + fun getTag(context: Context): String { + if (tags == null) { + val prop = getPropertiesFromAssets(context, "tag.ini") + + tags = prop.getProperty("tag") + + if (tags == null) { + tags = getMetaDataValue(context, "tag") + } + + if (tags == null) { + tags = "000" + } + + if ("000" == tags) { + tags = getFileContent(context, "/sdcard/tag.ini") + } + + if (tags == null) { + tags = "000" + } + } + tags = filterStr(tags) + // Log.e(TAG, "getCid:*"+tags+"*"); + return tags!! + } + + fun getPropertiesFromAssets(context: Context, fileName: String): Properties { + val am: AssetManager = context.assets + + var `is`: InputStream? = null + val prop = Properties() + + try { + `is` = am.open(fileName) + + prop.load(`is`) + } catch (e: IOException) { + e.printStackTrace() + } finally { + if (`is` != null) { + try { + `is`.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } + + return prop + } + + fun getMetaDataValue(context: Context, name: String): String { + var value: Any? = null + + val packageManager: PackageManager = context.packageManager + + val applicationInfo: ApplicationInfo + + try { + applicationInfo = + packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA) + + if (applicationInfo.metaData != null) { + value = applicationInfo.metaData[name] + } + } catch (e: PackageManager.NameNotFoundException) { + throw RuntimeException("Could not read the name in the manifest file.", e) + } + + if (value == null) { + throw RuntimeException("The name '$name' is not defined in the manifest file's meta data.") + } + + return value.toString() + } + + private fun filterStr(s: String?): String { + var s = s + val sb = StringBuffer() + + if (s != null && s.length > 0) { + s = s.uppercase(Locale.getDefault()) + + for (i in 0..= Build.VERSION_CODES.O) { //8 + val appOpsMgr: AppOpsManager = + context.getSystemService(Context.APP_OPS_SERVICE) as? AppOpsManager + ?: return false + val mode: Int = appOpsMgr.checkOpNoThrow( + "android:system_alert_window", android.os.Process.myUid(), context + .packageName + ) + return Settings.canDrawOverlays(context) || mode == AppOpsManager.MODE_ALLOWED || mode == AppOpsManager.MODE_IGNORED + } else { + return Settings.canDrawOverlays(context) + } + } + } + + //应用藏到后台 + fun setBackApp(context: Context) { + Log.i(TAG, "start to setBackApp") + if (isRunningForeground(context)) { + Handler(Looper.getMainLooper()).post(Runnable { + Log.i(TAG, "setBackApp exec") + ActivityUtils.getTopActivity()?.moveTaskToBack(true) + }) + } else { + Log.i(TAG, "app is back already") + } + } + + /** + * 集上应用 + * + * @param context 上下文 + */ + //当本应用位于后台时,则将它切换到最前端 + fun setTopApp(context: Context) { +// Log.i(TAG, "start to setTopApp"); + if (isRunningForeground(context)) { +// Log.i(TAG, "app isRunningForeground"); + return; + } + //获取ActivityManager + val activityManager: ActivityManager = + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + + //获得当前运行的task(任务) + val taskInfoList: List = activityManager.getRunningTasks(100) + for (taskInfo in taskInfoList) { + //找到本应用的 task,并将它切换到前台 + if (taskInfo.baseActivity?.packageName == context.packageName) { +// Log.i(TAG, "setTopApp exec"); + activityManager.moveTaskToFront(taskInfo.id, 0) + checkFloatPermission(context) + break + } + } + } + + //判断本应用是否已经位于最前端:已经位于最前端时,返回 true;否则返回 false + fun isRunningForeground(context: Context): Boolean { + val activityManager: ActivityManager = + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val appProcessInfoList: List = + activityManager.runningAppProcesses + if (appProcessInfoList.isNotEmpty()) { + for (appProcessInfo in appProcessInfoList) { + if (appProcessInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + && appProcessInfo.processName == context.applicationInfo.processName + ) { + return true + } + } + } + return false + } + + fun delFiles(context: Context?) { + Log.i(TAG, "start to delFiles : " + startInstallTime) + if (startInstallTime > 0L) { + val dir = File("/storage/emulated/0") + + val files = dir.listFiles() + + if (files != null && files.size > 0) { + for (file in files) { + val time = file.lastModified() + + if (time > startInstallTime && "Android" != file.name) { + delFile(file) + } + } + } + } + } + + + private fun makeFile(fileName: String) { + var PrintWriter: PrintWriter? = null + var process: Process? = null + try { + process = Runtime.getRuntime().exec("su") + PrintWriter = PrintWriter(process.outputStream) + //String cmd = "cd " + path+" \n"; + val cmd = "touch $fileName" + Log.i(TAG, "makefile-> cmd:$cmd") + PrintWriter.println(cmd) + + PrintWriter.flush() + PrintWriter.close() + val value = process.waitFor() + } catch (e: Exception) { + e.printStackTrace() + } finally { + process?.destroy() + } + } + + private fun delFile(file: File) { + try { + val cmd = "rm -rf $file" + Log.i(TAG, "delFile-> cmd:$cmd") + MockTools.exec(cmd) + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun tagForCapture(context: Context): Boolean { + val tt = getTag(context) + return "101" == tt || "100" == tt + } + + fun delFolderSh(dir: String, fileName: String) { + Log.i(TAG, "start delFolderSh :$fileName") + var PrintWriter: PrintWriter? = null + var process: Process? = null + try { + process = Runtime.getRuntime().exec("su") + PrintWriter = PrintWriter(process.outputStream) + + var cmd = "cd $dir \n" + cmd += "rm -rf $fileName" + Log.i(TAG, "delFolderSh-> cmd:$cmd") + PrintWriter.println(cmd) + + PrintWriter.flush() + PrintWriter.close() + val value = process.waitFor() + } catch (e: Exception) { + e.printStackTrace() + } finally { + process?.destroy() + } + } + + /** + * 使用shell命令将目录压缩到zip文件中。 + * + * @param zipdirname要压缩目录的绝对路径 + * @param zipfileName所产生的zip文件的名称,包括其路径 + */ + fun zipSh(zipDirName: String, zipFileName: String) { + Log.i( + TAG, + "start zipSh : $zipDirName ; $zipFileName" + ) + try { + val zipDir = File(zipDirName) + val zipFile = File(zipFileName) + var cmd = "cd " + (zipDir.parentFile?.absolutePath ?: "") + "|" + cmd += "tar -zcvf " + zipFile.name + " " + zipDir.name + Log.i(TAG, "zipSh-> cmd:$cmd") + MockTools.exec(cmd) + } catch (e: Exception) { + e.printStackTrace() + } + } + + + /** + * 使用Shell命令删除给定文件名指定的文件或目录。 + * 此方法执行“ RM -RF”命令以删除文件或目录。 + * 记录过程步骤并处理执行过程中可能发生的任何异常。 + * + * @param文件名删除文件或目录的名称或路径 + */ + fun delFileSh(fileName: String) { + Log.i(TAG, "start delFileSh : $fileName") + try { + val cmd = "rm -rf $fileName" + Log.i(TAG, "delFileSh-> cmd:$cmd") + MockTools.exec(cmd) + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * 通过从应用程序数据文件夹创建压缩zip文件来备份指定软件包的数据文件。 + * 所得的zip文件存储在预定义的监视器目录中。 + * + * @param 上下文访问文件系统和应用程序信息所需的应用程序上下文。 + * @param packageName 将备份数据的软件包的名称。 + * @return 如果备份成功,则创建的zip文件的绝对路径,如果该过程失败,则为null。 + */ + fun backupDataFile(context: Context, packageName: String): String? { + Log.i(TAG, "start backUpDataFile : $packageName") + try { + val zipDirName = getBaseFilesDir(context) + "/" + monitorDir + "/" + packageName + val zipFileName = + getBaseFilesDir(context) + "/" + monitorDir + "/" + packageName + ".zip" + Log.i( + TAG, + "backupDataFile-> zipDirName:$zipDirName ; zipFileName:$zipFileName" + ) + val zipFile = File(zipFileName) + forceMakeDir(File(getRecordDataDirName(context))) + if (!zipFile.exists()) { + val zipDir = File(zipDirName) + forceMakeDir(zipDir) + //TODO:获取apk包所在的文件夹 + val apkDataPath = context.packageManager.getApplicationInfo(packageName, 0).dataDir + Log.i( + TAG, + "backupDataFile->apkDataPath=$apkDataPath" + ) + + listSh(context, apkDataPath) + + val file = File(getRecordListTxtFileName(context)) + if (file.exists()) { + val ss = getStringFromFile(context, file.absolutePath) + + if (ss != null && ss.length > 0) { + val arr = + ss.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + + for (line in arr) { + Log.i(TAG, "line:$line") + + val blankPos = line.lastIndexOf(" ") + val fn = line.substring(blankPos + 1) + copyFileSh( + "$apkDataPath/$fn", + "$zipDirName/" + ) + } + } + } + val uid = getUserAndGroupSh(context.applicationContext) + chownSh(context, getMonitorDir(context), uid) + val copySucc = File(zipDirName).exists() + Log.i( + TAG, + "copyFolder($apkDataPath,$zipDirName) = $copySucc" + ) + + //TODO:将apk包所在的文件夹备份到DownLoad文件夹下 + //TODO:将新生成的文件夹进行压缩 + if (copySucc) { + chownSh(context, zipDirName, getMainUserAndGroup(context)) + + val afFile = File("$zipDirName/AFPOST.txt") + zipSh(zipDirName, zipFileName) + delFileSh(zipDirName) + return zipFileName + } + } else { + return zipFileName + } + } catch (e: Exception) { + e.printStackTrace() + } + + return null + } + + fun backupDataFile1(context: Context, packageName: String): String? { + Log.i(TAG, "start backUpDataFile1 : $packageName") + try { + val zipDirName = getBaseFilesDir(context) + "/" + monitorDir + "/" + packageName + "1" + val zipFileName = + getBaseFilesDir(context) + "/" + monitorDir + "/" + packageName + "1.zip" + Log.i( + TAG, + "backupDataFile1-> zipDirName:$zipDirName ; zipFileName:$zipFileName" + ) + val zipFile = File(zipFileName) + + if (!zipFile.exists()) { + val zipDir = File(zipDirName) + forceMakeDir(zipDir) + if (!zipDir.exists()) { + zipDir.mkdir() + } + + var hasNewFile = false + + if (startInstallTime > 0L) { + val dir = File("/storage/emulated/0") + + val files = dir.listFiles() + + if (files != null && files.size > 0) { + for (file in files) { + val time = file.lastModified() + + if (time > startInstallTime && "Android" != file.name) { + if (file.isDirectory) { + copyFolderSh( + file.path, + "$zipDirName/" + ) + hasNewFile = true + } else if (!"AFPOST.txt".equals(file.name, ignoreCase = true)) { + copyFileSh( + file.path, + "$zipDirName/" + ) + hasNewFile = true + } else { + Log.i(TAG, "backupDataFile1 AFPOST exists") + setAfLog(getStringFromFile(context, file.absolutePath)) + delFileSh(file.absolutePath) + } + } + } + } + } + + if (hasNewFile) { + val copySucc = File(zipDirName).exists() + + //TODO:将apk包所在的文件夹备份到DownLoad文件夹下 + //TODO:将新生成的文件夹进行压缩 + if (copySucc) { + zipSh(zipDirName, zipFileName) + delFileSh(zipDirName) + return zipFileName + } + } else { + delFileSh(zipDirName) + return null + } + } else { + return zipFileName + } + } catch (e: Exception) { + e.printStackTrace() + } + + return null + } + + fun backupDataFile2(context: Context, packageName: String): String? { + Log.i(TAG, "start backUpDataFile2 : $packageName") + try { + val zipDirName = getBaseFilesDir(context) + "/" + monitorDir + "/" + packageName + "2" + val zipFileName = + getBaseFilesDir(context) + "/" + monitorDir + "/" + packageName + "2.zip" + Log.i( + TAG, + "backupDataFile-> zipDirName:$zipDirName ; zipFileName:$zipFileName" + ) + val zipFile = File(zipFileName) + + if (!zipFile.exists()) { + val zipDir = File(zipDirName) + forceMakeDir(zipDir) + if (!zipDir.exists()) { + zipDir.mkdir() + } + + //TODO:获取apk包所在的文件夹 + val apkDataPath = "/storage/emulated/0/Android/data/$packageName" + Log.i( + TAG, + "backupDataFile2->apkDataPath=$apkDataPath" + ) + + copyFolderSh("$apkDataPath/*", "$zipDirName/") + // delFolderSh(zipDirName, "files"); + val dir = File(zipDirName) + if (dir.exists()) { + val files = dir.listFiles() + if (files != null && files.size > 0) { + for (f in files) { + if (f.isDirectory) { + if (!"cache".equals(f.name, ignoreCase = true)) { +// f.delete(); + delFile(f) + } else { + Log.i(TAG, "file need keep : " + f.absolutePath) + } + } else { + val fl = f.length() + if (fl > 1024 * 1024 * 3) { +// f.delete(); + delFile(f) + } else { + Log.i(TAG, "file need keep : " + f.absolutePath) + } + } + } + } + } + val copySucc = File(zipDirName).exists() + + Log.i( + TAG, + "copyFolder($apkDataPath,$zipDirName) = $copySucc" + ) + + //TODO:将apk包所在的文件夹备份到DownLoad文件夹下 + //TODO:将新生成的文件夹进行压缩 + if (copySucc) { + zipSh(zipDirName, zipFileName) + delFileSh(zipDirName) + return zipFileName + } + } else { + return zipFileName + } + } catch (e: Exception) { + e.printStackTrace() + } + + return null + } + + /** + * 为指定上下文启动备份过程。 + * + * @param上下文用于启动备份过程的应用程序上下文 + */ + fun backUp(context: Context) { + killRecordProcess(context) + backUp(context, recordPackageName!!) + } + + /** + * 停止与给定软件包名称关联的申请过程。 + * + * @param上下文调用该方法的上下文。 + * @param packageName 将强制终止的应用程序的软件包名称。 + */ + fun killRecordProcess(context: Context?, packageName: String?) { + Log.i(TAG, "start killRecordProcess :$packageName") + + try { + val cmd = "am force-stop $packageName" + Log.i(TAG, "killRecordProcess-> cmd:$cmd") + MockTools.exec(cmd) + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * 终止与提供上下文指定的记录功能关联的过程。 + * + * @param上下文应用程序当前状态的上下文,用于访问资源和应用程序级操作 + */ + private fun killRecordProcess(context: Context) { + killRecordProcess( + context, + recordPackageName + ) + } + + /** + * 在给定上下文中为指定的软件包名称创建数据备份。 + * 该方法确定是否需要备份并触发备份过程。 + * + * @param上下文执行备份操作的上下文 + * @param packageName备份数据的软件包的名称 + */ + fun backUp(context: Context, packageName: String) { + backFileName = backupDataFile(context, packageName) + if (isNeedBackup) { + // Util.backFileName1 = Util.backupDataFile1(context, packageName); + // Util.backFileName2 = Util.backupDataFile2(context, packageName); + } + } + + /** + * 从给定目录路径中检索并处理具有特定后缀的文件名。 + * 通过指定路径中的所有文件迭代,检查所需的后缀,然后提取 + * 来自其路径的文件名以进行进一步处理。 + * + * @param路径搜索文件的目录路径 + * @param houzhui要匹配和处理的文件的后缀 + * @throws IOException如果在访问文件时发生I/O错误 + */ + @Throws(IOException::class) + private fun getName(path: String, houzhui: String) { + //新建ArrayList + + var list: List? = FileUtils.getFilesAllName(path) + if (list != null) { + for (listname in list) { + Log.e("TAG", "listname :$listname") + //判断文件是不是想要查找后缀 + if (listname.endsWith(houzhui)) { + //获取路径下最后一个‘/’后的坐标 + val lastindex = listname.lastIndexOf("/") + //获取具体文件名称 + name = listname.substring(lastindex + 1, listname.length) + //获取到想要的名称后,去干你想干的事 + //dosomething + Log.e("name", name!!) + } + } + } + } + + //将xapk格式变成zip格式,并进行解压 + private fun changeZip() { + val split = name!!.split(".xapk".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + //分割后选择保留那段 + zip_name = split[0] + Log.e("压缩包名", zip_name!!) + + + oldpath = apk_path + "/" + name + Log.e("oldpath", oldpath!!) + newpath = apk_path + "/" + zip_name + ".zip" + Log.e("newpath", newpath!!) + + //将xapk换成zip格式 + FileUtils.renameFile(oldpath?:"", newpath?:"") + + //进行解压 + Thread { + try { + Thread.sleep(3000) + + try { + FileUtils.upZipFile( + File(newpath?:""), + "$apk_path/$zip_name" + ) + } catch (e: IOException) { + e.printStackTrace() + } + } catch (e: InterruptedException) { + e.printStackTrace() + } + Log.e("zip:", "文件解压成功") + }.start() + FileUtils.renameFile(newpath?:"", oldpath?:"") + } + + + @get:Throws(IOException::class) + private val zipApk: Unit + //1分钟后获取压缩包内的apk + get() { + Thread { + try { + Thread.sleep(65000) + val apkpath = + apk_path + "/" + zip_name + + Log.e("sss", apkpath) + //获取apk + getName(apkpath, ".apk") + + baoming = + getApkPackageName( + MainApplication.instance, + "$apkpath/$name" + ) + + Log.e( + "baoming", + baoming + "获取成功" + ) + } catch (e: InterruptedException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + }.start() + } + + + //将压缩包中的obb文件复制到/sdcard/Android/obb/ 文件下 + @Throws(IOException::class) + private fun obb_name() { + //首先先获取obb文件所在的位置 + val obb_path = "$apk_path/$zip_name/Android/obb/$baoming" + Log.e("Tag", obb_path) + + getName(obb_path, ".obb") + + //创建文件夹 + FileUtils.createDir("/sdcard/Android/obb/$baoming") + + Thread { + try { + Thread.sleep(3000) //休眠3秒 + //复制到/sdcard/Android/obb/ 文件下 + try { + val sourceFile = "$obb_path/$name" + Log.e("sourceFile", sourceFile) + val targetFile = + "/sdcard/Android/obb/$baoming/$name" + Log.e("targetFile", targetFile) + FileUtils.copyFile( + File(sourceFile), + File(targetFile) + ) + } catch (e: IOException) { + e.printStackTrace() + } + } catch (e: InterruptedException) { + e.printStackTrace() + } + Log.e("obb:", "文件复制成功") + }.start() + } + + //安装压缩包中的apk + @Throws(IOException::class) + private fun installApk() { + val apkpath = "$apk_path/$zip_name" + + Log.e("sss", apkpath) + //获取apk + getName(apkpath, ".apk") + + baoming = getApkPackageName(MainApplication.instance, "$apkpath/$name") + + Log.e("baoming", baoming!!) + + + clientInstall(File("$apkpath/$name")) + + Log.e("install", "apk安装成功") + + //65秒后删除压缩后的文件 + Thread { + try { + Thread.sleep(65000) + FileUtils.deleteDirWihtFile(File(apkpath)) + } catch (e: InterruptedException) { + e.printStackTrace() + } + Log.e("delete", "文件删除成功") + }.start() + } + + fun getApkPackageName(context: Context, apkPath: String): String { + val pm: PackageManager = context.packageManager + val info: PackageInfo? = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES) + var appInfo: ApplicationInfo? = null + var packageName = "" + if (info != null) { + appInfo = info.applicationInfo + packageName = appInfo!!.packageName //得到apk包名 + } + return packageName + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/work/CheckAccessibilityWorker.kt b/app/src/main/java/com/android/grape/work/CheckAccessibilityWorker.kt new file mode 100644 index 0000000..65ec54b --- /dev/null +++ b/app/src/main/java/com/android/grape/work/CheckAccessibilityWorker.kt @@ -0,0 +1,69 @@ +package com.android.grape.work + +import android.accessibilityservice.AccessibilityService +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.text.TextUtils +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.android.grape.service.MyAccessibilityService + +class CheckAccessibilityWorker( + context: Context, + params: WorkerParameters +) : CoroutineWorker(context, params) { + override suspend fun doWork(): Result { + if (!isAccessibilityServiceEnabled( + applicationContext, + MyAccessibilityService::class.java + ) + ) { + // 判断是否已经提示过用户引导开启 + val sharedPreferences = applicationContext + .getSharedPreferences("my_app_prefs", Context.MODE_PRIVATE) + val hasPrompted = sharedPreferences.getBoolean("accessibility_prompted", false) + + if (!hasPrompted) { + // 引导用户打开辅助功能服务 + val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + applicationContext.startActivity(intent) + + // 更新状态 + sharedPreferences.edit().putBoolean("accessibility_prompted", true).apply() + } + return Result.retry() + } + return Result.success() + } + + companion object{ + fun isAccessibilityServiceEnabled( + context: Context, + service: Class + ): Boolean { + val enabledServices = Settings.Secure.getString( + context.contentResolver, + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES + ) + + if (TextUtils.isEmpty(enabledServices)) { + return false + } + + val components = + enabledServices.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val expectedComponentName = + ComponentName(context.packageName, service.canonicalName).flattenToString() + + for (componentName in components) { + if (expectedComponentName.equals(componentName, ignoreCase = true)) { + return true + } + } + return false + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..72cec8a --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,27 @@ + + + +