├── .github └── workflows │ └── manual_release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── cn │ │ └── tinyhai │ │ └── ban_uninstall │ │ ├── auth │ │ ├── IAuth.aidl │ │ └── entities │ │ │ └── OpRecord.aidl │ │ └── transact │ │ └── ITransactor.aidl │ ├── assets │ └── xposed_init │ ├── java │ └── cn │ │ └── tinyhai │ │ └── ban_uninstall │ │ ├── App.kt │ │ ├── AuthActivity.kt │ │ ├── MainActivity.kt │ │ ├── XposedInit.kt │ │ ├── auth │ │ ├── client │ │ │ └── AuthClient.kt │ │ ├── entities │ │ │ ├── AuthData.kt │ │ │ ├── OpRecord.kt │ │ │ ├── OpResult.kt │ │ │ └── OpType.kt │ │ └── server │ │ │ ├── AuthHelper.kt │ │ │ ├── AuthService.kt │ │ │ ├── OpRecordList.kt │ │ │ └── PendingOpList.kt │ │ ├── configs │ │ └── Configs.kt │ │ ├── hooker │ │ ├── Constants.kt │ │ ├── HookGetSystemContext.kt │ │ ├── HookInjectSelf.kt │ │ ├── HookSelf.kt │ │ └── HookSystem.kt │ │ ├── receiver │ │ ├── BootCompletedReceiver.kt │ │ ├── PackageChangeReceiver.kt │ │ └── RestartMainReceiver.kt │ │ ├── transact │ │ ├── client │ │ │ └── TransactClient.kt │ │ ├── entities │ │ │ ├── ActiveMode.kt │ │ │ └── PkgInfo.kt │ │ └── server │ │ │ ├── BannedPkgHelper.kt │ │ │ └── TransactService.kt │ │ ├── ui │ │ ├── component │ │ │ ├── Dialog.kt │ │ │ ├── SearchAppBar.kt │ │ │ └── Tooltip.kt │ │ ├── compositionlocal │ │ │ └── LocalDateFormat.kt │ │ ├── screen │ │ │ ├── BannedAppScreen.kt │ │ │ ├── MainScreen.kt │ │ │ └── OpRecordScreen.kt │ │ └── theme │ │ │ ├── Colors.kt │ │ │ ├── Theme.kt │ │ │ └── Typo.kt │ │ ├── utils │ │ ├── AssetsEx.kt │ │ ├── BiometricUtils.kt │ │ ├── Bugreport.kt │ │ ├── Cli.kt │ │ ├── FileUtils.kt │ │ ├── HandlerUtils.kt │ │ ├── HanziToPinyin.java │ │ ├── SPHost.kt │ │ ├── SharedPrefs.kt │ │ ├── SystemContextHolder.kt │ │ ├── XPLogUtils.kt │ │ └── XSharedPrefs.kt │ │ └── vm │ │ ├── AuthViewModel.kt │ │ ├── BannedAppViewModel.kt │ │ ├── MainViewModel.kt │ │ ├── OpRecordViewModel.kt │ │ └── VMEx.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values-en │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── scope.xml │ └── strings.xml │ └── xml │ └── filepaths.xml ├── build.gradle.kts ├── crowdin.yml ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hiddenApi ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── android │ ├── app │ ├── ActivityThread.java │ └── IActivityManager.java │ ├── content │ └── pm │ │ ├── BaseParceledListSlice.java │ │ ├── IPackageDataObserver.java │ │ ├── IPackageDeleteObserver2.java │ │ ├── IPackageManager.java │ │ └── ParceledListSlice.java │ ├── ddm │ └── DdmHandleAppName.java │ └── os │ ├── IUserManager.java │ └── ServiceManager.java ├── hook ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── cn │ └── tinyhai │ └── xp │ ├── annotation │ ├── HookScope.kt │ ├── HookerGate.kt │ ├── HookerId.kt │ ├── Initiate.kt │ ├── InjectHooker.kt │ ├── MethodHooker.kt │ └── Oneshot.kt │ └── hook │ ├── Hooker.kt │ ├── callback │ └── HookCallbacks.kt │ └── logger │ └── XPLogger.kt ├── processor ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── java │ └── cn │ │ └── tinyhai │ │ └── xp │ │ └── processor │ │ ├── Processor.kt │ │ ├── ProcessorProvider.kt │ │ ├── codegen │ │ ├── CodeGen.kt │ │ ├── GenerateHookScope.kt │ │ ├── GenerateHookerManager.kt │ │ ├── HookScopeInfo.kt │ │ └── HookerInfo.kt │ │ └── parser │ │ ├── HookScopeParser.kt │ │ ├── HookerInfoParser.kt │ │ └── Parser.kt │ └── resources │ └── META-INF │ └── services │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider ├── screenshots ├── screenshot1.jpg ├── screenshot2.jpg ├── screenshot3.jpg └── screenshot4.jpg └── settings.gradle.kts /.github/workflows/manual_release.yml: -------------------------------------------------------------------------------- 1 | name: Manual Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | draft: 6 | description: "Publish as draft" 7 | type: boolean 8 | default: true 9 | publish: 10 | description: "Publish to Xposed Module Repository" 11 | type: boolean 12 | default: false 13 | 14 | 15 | jobs: 16 | build_inject_tool: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checking out InjectTool 20 | uses: actions/checkout@v4 21 | with: 22 | repository: TinyHai/InjectTool 23 | fetch-depth: 0 24 | 25 | - name: Setup rustup 26 | run: | 27 | rustup update stable 28 | - uses: Swatinem/rust-cache@v2 29 | with: 30 | cache-targets: false 31 | 32 | - name: Install cross 33 | run: | 34 | cargo install cross --git https://github.com/cross-rs/cross --rev 66845c1 35 | 36 | - name: Build InejctTool 37 | run: | 38 | ./build.sh 39 | 40 | - name: Upload InjectTool 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: InjectTool 44 | path: out/ 45 | 46 | build_lspatch: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Checking out LSPatch 50 | uses: actions/checkout@v4 51 | with: 52 | repository: TinyHai/LSPatch 53 | ref: 'dev' 54 | submodules: 'recursive' 55 | fetch-depth: 0 56 | 57 | - name: Checkout libxposed/api 58 | uses: actions/checkout@v4 59 | with: 60 | ref: 'a42f85d06eac3373d266a534ab3b31a584b30774' 61 | repository: libxposed/api 62 | path: libxposed/api 63 | 64 | - name: Checkout libxposed/service 65 | uses: actions/checkout@v4 66 | with: 67 | ref: '4351a735755c86c031a977a62e52005b23048c4d' 68 | repository: libxposed/service 69 | path: libxposed/service 70 | 71 | - name: Setup Java 72 | uses: actions/setup-java@v3 73 | with: 74 | java-version: '17' 75 | distribution: 'temurin' 76 | 77 | - name: Setup Gradle 78 | uses: gradle/gradle-build-action@v2 79 | with: 80 | gradle-home-cache-cleanup: true 81 | 82 | - name: Set up ccache 83 | uses: hendrikmuhs/ccache-action@v1.2 84 | with: 85 | max-size: 2G 86 | key: ${{ runner.os }} 87 | restore-keys: ${{ runner.os }} 88 | save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} 89 | 90 | - name: Build dependencies 91 | working-directory: libxposed 92 | run: | 93 | cd api 94 | echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties 95 | ./gradlew :api:publishApiPublicationToMavenLocal 96 | cd .. 97 | cd service 98 | echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties 99 | ./gradlew :interface:publishInterfacePublicationToMavenLocal 100 | 101 | - name: Build with Gradle 102 | run: | 103 | echo 'org.gradle.parallel=true' >> gradle.properties 104 | echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties 105 | echo 'android.native.buildOutput=verbose' >> gradle.properties 106 | ./gradlew buildAll 107 | - name: Upload Release Assets 108 | uses: actions/upload-artifact@v4 109 | with: 110 | name: LSPatchReleaseAssets 111 | path: out/assets/release/ 112 | - name: Upload Debug Assets 113 | uses: actions/upload-artifact@v4 114 | with: 115 | name: LSPatchDebugAssets 116 | path: out/assets/debug/ 117 | 118 | 119 | build_apk: 120 | needs: [build_inject_tool, build_lspatch] 121 | runs-on: ubuntu-latest 122 | steps: 123 | - name: Checking out branch 124 | uses: actions/checkout@v4 125 | 126 | - name: Download LSPatchReleaseAssets 127 | uses: actions/download-artifact@v4 128 | with: 129 | name: LSPatchReleaseAssets 130 | path: app/src/main/assets 131 | - name: Download InjectTool 132 | uses: actions/download-artifact@v4 133 | with: 134 | name: InjectTool 135 | path: app/src/main/jniLibs 136 | 137 | - name: Setup Java 138 | uses: actions/setup-java@v4 139 | with: 140 | distribution: 'temurin' 141 | java-version: 17 142 | 143 | - name: Setup Gradle 144 | uses: gradle/gradle-build-action@v3 145 | with: 146 | gradle-home-cache-cleanup: true 147 | 148 | - name: Decode Keystore 149 | run: | 150 | echo "${{ secrets.KEYSTORE_BASE64 }}" >> jks.base64 151 | base64 -d jks.base64 > keystore.jks 152 | 153 | - name: Build with Gradle 154 | env: 155 | KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} 156 | KEY_ALIAS: ${{ secrets.KEY_ALIAS }} 157 | KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} 158 | run: | 159 | ./gradlew clean buildRelease 160 | 161 | - name: Generate tag name 162 | run: | 163 | VERSION_CODE=$(cat ./app/build/outputs/apk/release/versionCode) 164 | VERSION_NAME=$(cat ./app/build/outputs/apk/release/versionName) 165 | echo "TAG_NAME=$VERSION_CODE-$VERSION_NAME" >> $GITHUB_ENV 166 | 167 | - name: Upload build artifact 168 | uses: actions/upload-artifact@v4 169 | with: 170 | name: BanUninstall 171 | path: app/build/outputs/apk/**/*.apk 172 | 173 | - name: Release to current repository 174 | uses: softprops/action-gh-release@v2 175 | with: 176 | name: ${{ format('BanUninstall_{0}', env.TAG_NAME) }} 177 | draft: ${{ inputs.draft }} 178 | tag_name: ${{ env.TAG_NAME }} 179 | body_path: ${{ github.workspace }}/CHANGELOG.md 180 | token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 181 | files: | 182 | app/build/outputs/apk/**/*.apk 183 | 184 | - name: Release to xposed module repository 185 | uses: softprops/action-gh-release@v2 186 | if: ${{ inputs.publish }} 187 | with: 188 | name: ${{ format('BanUninstall_{0}', env.TAG_NAME) }} 189 | tag_name: ${{ env.TAG_NAME }} 190 | body_path: ${{ github.workspace }}/CHANGELOG.md 191 | repository: Xposed-Modules-Repo/cn.tinyhai.ban_uninstall 192 | token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 193 | files: | 194 | app/build/outputs/apk/release/*.apk -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | build.sh 9 | 10 | # Log/OS Files 11 | *.log 12 | 13 | # Android Studio generated files and folders 14 | captures/ 15 | .externalNativeBuild/ 16 | .cxx/ 17 | *.apk 18 | output.json 19 | 20 | # IntelliJ 21 | *.iml 22 | .idea/ 23 | misc.xml 24 | deploymentTargetDropDown.xml 25 | render.experimental.xml 26 | 27 | # Keystore files 28 | *.jks 29 | *.keystore 30 | 31 | # Google Services (e.g. APIs or Firebase) 32 | google-services.json 33 | 34 | # Android Profiling 35 | *.hprof 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | - 添加操作记录功能 2 | - 一些UI调整 3 | - 更新版本号 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BanUninstall 2 | 3 | 一个禁止卸载和禁止清除用户数据的Xposed模块 & 支持使用Root激活 4 | 5 | A Xposed Module that prevents apps be uninstalled or apps' data be cleared. Also support activate with root. 6 | 7 | ### Translations 8 | To translate BanUninstall to your language, please use [Crowdin](https://crowdin.com/project/banuninstall) 9 | 10 | 11 | ### Screenshots 12 | | 1 | 2 | 3 | 4 | 13 | |:------------------------------------------:|:----------------------------------------:|:----------------------------------------:|:----------------------------------------:| 14 | | | | | | 15 | 16 | 17 | ### Compatibility 18 | Xposed: 19 | 20 | Android 5.0 - Android 14 21 | 22 | > Please don't activate with root when any Xposed Framework is running 23 | 24 | Root: 25 | 26 | Android 8.1 - Android 14 27 | 28 | 29 | I have tested with my phone on Android 14, and it works well. I can't ensure it works on your phone. 30 | 31 | **So before you use it, you must test it with an irrelevant app by yourself.** 32 | 33 | **I am not responsible for any data loss if you test it using an important app.** 34 | 35 | ### Credits 36 | Activate with Root via [LSPatch](https://github.com/LSPosed/LSPatch) 37 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | 3 | **/assets/lspatch 4 | 5 | **/jniLibs/**/*.so -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.support.uppercaseFirstChar 2 | import java.io.ByteArrayOutputStream 3 | 4 | plugins { 5 | alias(libs.plugins.androidApplication) 6 | alias(libs.plugins.jetbrainsKotlinAndroid) 7 | alias(libs.plugins.ksp) 8 | alias(libs.plugins.kotlinParcelize) 9 | alias(libs.plugins.composeCompiler) 10 | } 11 | 12 | fun String.execute(): String { 13 | val byteOut = ByteArrayOutputStream() 14 | project.exec { 15 | commandLine = this@execute.split("\\s".toRegex()) 16 | standardOutput = byteOut 17 | } 18 | return String(byteOut.toByteArray()).trim() 19 | } 20 | 21 | val allArch: Array by rootProject.ext 22 | 23 | android { 24 | namespace = "cn.tinyhai.ban_uninstall" 25 | compileSdk = 34 26 | 27 | signingConfigs { 28 | register("release") { 29 | storeFile = file("../keystore.jks") 30 | storePassword = System.getenv("KEYSTORE_PASSWORD") 31 | keyAlias = System.getenv("KEY_ALIAS") 32 | keyPassword = System.getenv("KEY_PASSWORD") 33 | } 34 | } 35 | 36 | defaultConfig { 37 | applicationId = "cn.tinyhai.ban_uninstall" 38 | minSdk = 21 39 | targetSdk = 34 40 | versionCode = 8 41 | versionName = "1.4.0" 42 | 43 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 44 | } 45 | 46 | buildTypes { 47 | debug { 48 | versionNameSuffix = "-${"git rev-parse --verify --short HEAD".execute()}_debug" 49 | } 50 | release { 51 | isMinifyEnabled = true 52 | proguardFiles( 53 | getDefaultProguardFile("proguard-android-optimize.txt"), 54 | "proguard-rules.pro" 55 | ) 56 | signingConfig = signingConfigs.getByName("release") 57 | versionNameSuffix = "-${"git rev-parse --verify --short HEAD".execute()}" 58 | } 59 | 60 | allArch.forEach { arch -> 61 | register(arch) { 62 | initWith(getByName("release")) 63 | versionNameSuffix = "${versionNameSuffix}_$arch" 64 | ndk { 65 | abiFilters.clear() 66 | abiFilters += if (arch == "universal") arrayOf( 67 | "arm64-v8a", 68 | "armeabi-v7a", 69 | "x86", 70 | "x86_64" 71 | ) else arrayOf(arch) 72 | } 73 | } 74 | } 75 | 76 | all { 77 | matchingFallbacks += "release" 78 | val fieldValue = when (this.name) { 79 | "debug", in allArch -> "true" 80 | else -> "false" 81 | } 82 | buildConfigField("boolean", "ROOT_FEATURE", fieldValue) 83 | } 84 | } 85 | 86 | packaging { 87 | jniLibs { 88 | useLegacyPackaging = true 89 | } 90 | } 91 | 92 | buildFeatures { 93 | compose = true 94 | buildConfig = true 95 | aidl = true 96 | } 97 | 98 | compileOptions { 99 | sourceCompatibility = JavaVersion.VERSION_17 100 | targetCompatibility = JavaVersion.VERSION_17 101 | } 102 | 103 | kotlinOptions { 104 | jvmTarget = "17" 105 | } 106 | } 107 | 108 | afterEvaluate { 109 | android.applicationVariants.all { 110 | val buildType = buildType.name 111 | val flavor = flavorName 112 | val versionCode = versionCode 113 | val versionName = this.versionName 114 | 115 | val apkName = buildString { 116 | append("app") 117 | if (flavor.isNotBlank()) { 118 | append("-$flavor") 119 | } 120 | append("-$buildType") 121 | append(".apk") 122 | } 123 | val newApkName = "${rootProject.name}_$versionName.apk" 124 | val apkPath = buildString { 125 | append(layout.buildDirectory.asFile.get().path) 126 | append("/outputs/apk/") 127 | if (flavor.isNotBlank()) { 128 | append("$flavor/") 129 | } 130 | append(buildType) 131 | } 132 | val apkFile = File(apkPath, apkName) 133 | 134 | val renameTask = 135 | tasks.register("rename${flavor.uppercaseFirstChar()}${buildType.uppercaseFirstChar()}Output") { 136 | doLast { 137 | apkFile.renameTo(File(apkPath, newApkName)) 138 | } 139 | } 140 | 141 | val assembleTask = 142 | tasks.findByName("assemble${flavor.uppercaseFirstChar()}${buildType.uppercaseFirstChar()}") 143 | assembleTask?.finalizedBy(renameTask) 144 | 145 | when (buildType) { 146 | "release" -> { 147 | assembleTask?.doLast { 148 | val versionCodeFile = File(apkPath, "versionCode") 149 | .apply { if (!exists()) createNewFile() } 150 | versionCodeFile.bufferedWriter().use { 151 | it.append(versionCode.toString()) 152 | it.flush() 153 | } 154 | val versionNameFile = File(apkPath, "versionName") 155 | .apply { if (!exists()) createNewFile() } 156 | versionNameFile.bufferedWriter().use { 157 | it.append(versionName) 158 | it.flush() 159 | } 160 | } 161 | 162 | tasks.findByName("strip${buildType.uppercaseFirstChar()}DebugSymbols")?.doLast { 163 | file(this.outputs.files.asPath).let { if (it.exists()) it.deleteRecursively() } 164 | } 165 | 166 | tasks.findByName("merge${flavor.uppercaseFirstChar()}${buildType.uppercaseFirstChar()}Assets") 167 | ?.doLast { 168 | val lspatchDir = file(this.outputs.files.asPath).resolve("lspatch") 169 | if (lspatchDir.exists()) { 170 | lspatchDir.deleteRecursively() 171 | } 172 | } 173 | } 174 | 175 | in allArch -> { 176 | tasks.findByName("merge${flavor.uppercaseFirstChar()}${buildType.uppercaseFirstChar()}Assets") 177 | ?.doLast { 178 | val soDir = file(this.outputs.files.asPath).resolve("lspatch").resolve("so") 179 | soDir.listFiles()?.filter { it.isDirectory }?.forEach { 180 | if (it.name != buildType) { 181 | it.deleteRecursively() 182 | } 183 | } 184 | } 185 | } 186 | 187 | "universal", "debug" -> {} 188 | } 189 | } 190 | } 191 | 192 | dependencies { 193 | compileOnly(libs.xposed.api) 194 | compileOnly(project(":hiddenApi")) 195 | implementation(platform(libs.androidx.compose.bom)) 196 | implementation(libs.androidx.activity.compose) 197 | implementation(libs.appcompat) 198 | implementation(libs.androidx.biometric) 199 | implementation(libs.androidx.compose.material3) 200 | implementation(libs.androidx.compose.material) 201 | implementation(libs.androidx.compose.material.icons.extended) 202 | implementation(libs.androidx.compose.ui.tooling.preview) 203 | implementation(libs.composeSettings.ui) 204 | implementation(libs.composeSettings.ui.extended) 205 | implementation(libs.dev.rikka.rikkax.parcelablelist) 206 | implementation(libs.coil.compose) 207 | implementation(libs.compose.destinations.core) 208 | implementation(libs.compose.destinations.bottomsheet) 209 | implementation(libs.libsu.core) 210 | implementation("com.github.TinyHai:ComposeDragDrop:dev-SNAPSHOT") 211 | implementation(project(":hook")) 212 | ksp(project(":processor")) 213 | ksp(libs.compose.destinations.ksp) 214 | debugImplementation(libs.androidx.compose.ui.tooling) 215 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep public class cn.tinyhai.ban_uninstall.XposedInit 24 | 25 | -keep class cn.tinyhai.ban_uninstall.App { 26 | public getXpTag(); 27 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 20 | 21 | 24 | 27 | 30 | 33 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 54 | 55 | 59 | 60 | 61 | 62 | 63 | 64 | 69 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/aidl/cn/tinyhai/ban_uninstall/auth/IAuth.aidl: -------------------------------------------------------------------------------- 1 | // IAuth.aidl 2 | package cn.tinyhai.ban_uninstall.auth; 3 | 4 | import cn.tinyhai.ban_uninstall.auth.entities.OpRecord; 5 | 6 | // Declare any non-default types here with import statements 7 | 8 | interface IAuth { 9 | boolean hasPwd(); 10 | void setPwd(String newSha256); 11 | void clearPwd(); 12 | boolean authenticate(String sha256); 13 | oneway void agree(int opId); 14 | oneway void prevent(int opId); 15 | boolean isValid(int opId); 16 | 17 | List getAllOpRecord(); 18 | void clearAllOpRecord(); 19 | } -------------------------------------------------------------------------------- /app/src/main/aidl/cn/tinyhai/ban_uninstall/auth/entities/OpRecord.aidl: -------------------------------------------------------------------------------- 1 | // OpRecord.aidl 2 | package cn.tinyhai.ban_uninstall.auth.entities; 3 | 4 | // Declare any non-default types here with import statements 5 | 6 | parcelable OpRecord; -------------------------------------------------------------------------------- /app/src/main/aidl/cn/tinyhai/ban_uninstall/transact/ITransactor.aidl: -------------------------------------------------------------------------------- 1 | // ITransactor.aidl 2 | package cn.tinyhai.ban_uninstall.transact; 3 | 4 | import rikka.parcelablelist.ParcelableListSlice; 5 | import android.content.pm.ApplicationInfo; 6 | 7 | // Declare any non-default types here with import statements 8 | 9 | interface ITransactor { 10 | ParcelableListSlice getPackages(); 11 | 12 | void banPackage(in List packageNames, out List bannedPackages); 13 | 14 | void freePackage(in List packageNames, out List freedPackages); 15 | 16 | List getAllBannedPackages(); 17 | 18 | String sayHello(String hello); 19 | 20 | oneway void onAppLaunched(); 21 | 22 | oneway void reloadPrefs(); 23 | 24 | IBinder getAuth(); 25 | 26 | int getActiveMode(); 27 | 28 | boolean syncPrefs(in Map prefs); 29 | 30 | ApplicationInfo getApplicationInfoAsUser(String packageName, int userId); 31 | } -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | cn.tinyhai.ban_uninstall.XposedInit -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/App.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall 2 | 3 | import android.app.Application 4 | 5 | class App : Application() { 6 | companion object { 7 | lateinit var app: App 8 | 9 | private const val TAG = "App" 10 | } 11 | 12 | override fun onCreate() { 13 | super.onCreate() 14 | app = this 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/AuthActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.graphics.Color 5 | import android.graphics.drawable.ColorDrawable 6 | import android.graphics.drawable.Drawable 7 | import android.os.Bundle 8 | import android.util.Log 9 | import android.view.Window 10 | import androidx.activity.addCallback 11 | import androidx.activity.compose.setContent 12 | import androidx.activity.viewModels 13 | import androidx.appcompat.app.AppCompatActivity 14 | import androidx.compose.foundation.layout.* 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material3.* 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.produceState 20 | import androidx.compose.runtime.rememberCoroutineScope 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.platform.LocalContext 24 | import androidx.compose.ui.res.stringResource 25 | import androidx.compose.ui.unit.dp 26 | import cn.tinyhai.ban_uninstall.auth.client.AuthClient 27 | import cn.tinyhai.ban_uninstall.auth.client.AuthClient.Companion.isDummy 28 | import cn.tinyhai.ban_uninstall.auth.entities.AuthData 29 | import cn.tinyhai.ban_uninstall.auth.entities.OpType 30 | import cn.tinyhai.ban_uninstall.ui.component.rememberVerifyPwdDialog 31 | import cn.tinyhai.ban_uninstall.ui.theme.AppTheme 32 | import cn.tinyhai.ban_uninstall.utils.BiometricUtils 33 | import cn.tinyhai.ban_uninstall.vm.AuthViewModel 34 | import coil.compose.AsyncImage 35 | import kotlinx.coroutines.launch 36 | 37 | private const val TAG = "AuthActivity" 38 | 39 | class AuthActivity : AppCompatActivity() { 40 | 41 | private val viewModel by viewModels() 42 | 43 | override fun onCreate(savedInstanceState: Bundle?) { 44 | supportRequestWindowFeature(Window.FEATURE_NO_TITLE) 45 | super.onCreate(savedInstanceState) 46 | setFinishOnTouchOutside(false) 47 | AuthClient.inject(intent) 48 | 49 | val authClient = AuthClient() 50 | val authData = AuthClient.parseAuthData(intent) 51 | if (authClient.isDummy || authData.isEmpty) { 52 | finish() 53 | return 54 | } 55 | 56 | viewModel.setup(authClient, authData) 57 | 58 | if (!viewModel.isValid()) { 59 | finish() 60 | return 61 | } 62 | 63 | onBackPressedDispatcher.addCallback { 64 | viewModel.onPrevent() 65 | finish() 66 | } 67 | 68 | Log.i(TAG, "$this: $authData") 69 | 70 | setContent { 71 | AppTheme { 72 | val scope = rememberCoroutineScope() 73 | val context = LocalContext.current 74 | val verifyPwdDialog = rememberVerifyPwdDialog( 75 | title = stringResource(id = R.string.title_verify_password), 76 | errorText = stringResource(R.string.text_verify_password_error), 77 | onVerify = { viewModel.authenticate(it) }, 78 | ) 79 | 80 | val isDual = authData.appInfo.uid / 100_000 > 0 81 | val appLabel = authData.appInfo.loadLabel(context.packageManager).toString().let { 82 | if (isDual) stringResource(R.string.text_app_label_dual, it) else it 83 | } 84 | 85 | val opText = when (authData.opType) { 86 | OpType.ClearData -> stringResource( 87 | id = R.string.text_clear_the_app_data, 88 | appLabel 89 | ) 90 | 91 | OpType.Uninstall -> stringResource( 92 | id = R.string.text_uninstall_the_app, 93 | appLabel 94 | ) 95 | } 96 | val authTitle = stringResource(R.string.title_auth) 97 | val authDescription = stringResource(R.string.description_auth, opText) 98 | ConfirmDialogContent( 99 | authData = viewModel.authData, 100 | onConfirm = { 101 | scope.launch { 102 | if (viewModel.hasPwd) { 103 | if (verifyPwdDialog.verify()) { 104 | finishOp(true) 105 | } 106 | } else { 107 | BiometricUtils.auth(context, authTitle, authDescription) 108 | .onSuccess { success -> 109 | if (success) { 110 | finishOp(true) 111 | } 112 | } 113 | } 114 | } 115 | }, 116 | onCancel = { 117 | finishOp(false) 118 | }, 119 | ) 120 | } 121 | } 122 | window.setBackgroundDrawableResource(android.R.color.transparent) 123 | } 124 | 125 | private fun finishOp(agree: Boolean) { 126 | if (agree) { 127 | viewModel.onAgree() 128 | } else { 129 | viewModel.onPrevent() 130 | } 131 | finish() 132 | } 133 | 134 | override fun finish() { 135 | super.finishAndRemoveTask() 136 | } 137 | } 138 | 139 | @Composable 140 | private fun ConfirmDialogContent( 141 | authData: AuthData, 142 | onConfirm: () -> Unit, 143 | onCancel: () -> Unit 144 | ) { 145 | val context = LocalContext.current 146 | val opType = authData.opType 147 | val unknownApp = stringResource(R.string.text_unknown_app) 148 | val rootLabel = stringResource(R.string.text_label_root) 149 | val shellLabel = stringResource(R.string.text_label_shell) 150 | val systemLabel = stringResource(R.string.text_label_system) 151 | val opLabel by produceState("") { 152 | value = authData.opAppInfo?.loadLabel(context.packageManager)?.toString() 153 | ?: when (authData.opUid) { 154 | 0 -> rootLabel 155 | 2000 -> shellLabel 156 | 1000 -> systemLabel 157 | else -> unknownApp 158 | } 159 | } 160 | val opTypeText = when (opType) { 161 | OpType.ClearData -> stringResource(R.string.text_clear_app_data) 162 | OpType.Uninstall -> stringResource(R.string.text_uninstall_app) 163 | } 164 | val isDual = authData.opUid / 100_000 > 0 165 | val opContentText = buildString { 166 | append(if (isDual) stringResource(id = R.string.text_app_label_dual, opLabel) else opLabel) 167 | append("(${authData.opUid}) ") 168 | append(stringResource(id = R.string.text_try_op, opTypeText.lowercase())) 169 | } 170 | Surface( 171 | shape = RoundedCornerShape(28.dp), 172 | ) { 173 | Column( 174 | modifier = Modifier 175 | .widthIn(280.dp, 560.dp) 176 | .padding(24.dp), 177 | ) { 178 | Text( 179 | text = opTypeText, 180 | style = MaterialTheme.typography.headlineSmall 181 | ) 182 | Spacer(modifier = Modifier.height(16.dp)) 183 | Text(text = opContentText, style = MaterialTheme.typography.bodyLarge) 184 | ProvideTextStyle(MaterialTheme.typography.bodyMedium) { 185 | AppInfoContent(authData.appInfo) 186 | } 187 | Spacer(modifier = Modifier.height(24.dp)) 188 | Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) { 189 | TextButton(onClick = { onCancel() }) { 190 | Text(text = stringResource(R.string.text_prevent)) 191 | } 192 | TextButton( 193 | onClick = { 194 | onConfirm() 195 | } 196 | ) { 197 | Text(text = stringResource(R.string.text_verify_and_allow)) 198 | } 199 | } 200 | } 201 | } 202 | } 203 | 204 | @Composable 205 | private fun AppInfoContent(appInfo: ApplicationInfo) { 206 | val context = LocalContext.current 207 | val icon by produceState(ColorDrawable(Color.TRANSPARENT)) { 208 | appInfo.loadIcon(context.packageManager)?.let { value = it } 209 | } 210 | val label by produceState("") { 211 | value = appInfo.loadLabel(context.packageManager).toString() 212 | } 213 | val isDual = appInfo.uid / 100_000 > 0 214 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp)) { 215 | AsyncImage( 216 | icon, 217 | contentDescription = label, 218 | modifier = Modifier.size(48.dp) 219 | ) 220 | Spacer(modifier = Modifier.width(8.dp)) 221 | Column { 222 | Text( 223 | text = if (isDual) stringResource( 224 | id = R.string.text_app_label_dual, 225 | label 226 | ) else label 227 | ) 228 | Text(text = appInfo.packageName) 229 | } 230 | } 231 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.activity.enableEdgeToEdge 9 | import cn.tinyhai.ban_uninstall.transact.client.TransactClient 10 | import cn.tinyhai.ban_uninstall.ui.theme.AppTheme 11 | import com.ramcosta.composedestinations.DestinationsNavHost 12 | import com.ramcosta.composedestinations.animations.defaults.DefaultFadingTransitions 13 | import com.ramcosta.composedestinations.generated.NavGraphs 14 | 15 | class MainActivity : ComponentActivity() { 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | Log.i("MainActivity", intent.toString()) 19 | TransactClient.inject(intent) 20 | enableEdgeToEdge() 21 | setContent { 22 | AppTheme { 23 | DestinationsNavHost(NavGraphs.root, defaultTransitions = DefaultFadingTransitions) 24 | } 25 | } 26 | } 27 | 28 | companion object { 29 | fun restart() { 30 | val intent = Intent(App.app, MainActivity::class.java).apply { 31 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 32 | } 33 | App.app.startActivity(intent) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/XposedInit.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall 2 | 3 | import cn.tinyhai.ban_uninstall.transact.entities.ActiveMode 4 | import cn.tinyhai.ban_uninstall.utils.XPLogUtils 5 | import cn.tinyhai.xp.hook.HookerManager 6 | import de.robv.android.xposed.IXposedHookLoadPackage 7 | import de.robv.android.xposed.callbacks.XC_LoadPackage 8 | 9 | 10 | class XposedInit : IXposedHookLoadPackage { 11 | 12 | companion object { 13 | val activeMode by lazy { 14 | val cl = XposedInit::class.java.classLoader 15 | try { 16 | cl!!.loadClass("cn.tinyhai.xposed.meta_loader.LoaderEntry") 17 | ActiveMode.Root 18 | } catch (e: Exception) { 19 | XPLogUtils.log(e) 20 | ActiveMode.Xposed 21 | } 22 | } 23 | } 24 | 25 | override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { 26 | XPLogUtils.log("handleLoadPackage: ${lpparam.packageName}, ${lpparam.processName}") 27 | HookerManager(XPLogUtils).startHook(lpparam) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/auth/client/AuthClient.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.auth.client 2 | 3 | import android.content.ComponentName 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.os.IBinder 7 | import android.util.Log 8 | import cn.tinyhai.ban_uninstall.AuthActivity 9 | import cn.tinyhai.ban_uninstall.BuildConfig 10 | import cn.tinyhai.ban_uninstall.auth.IAuth 11 | import cn.tinyhai.ban_uninstall.auth.entities.AuthData 12 | import cn.tinyhai.ban_uninstall.auth.entities.OpRecord 13 | import java.security.MessageDigest 14 | 15 | class AuthClient( 16 | private var service: IAuth 17 | ) : IAuth { 18 | 19 | override fun asBinder(): IBinder? { 20 | return service.asBinder() 21 | } 22 | 23 | override fun hasPwd(): Boolean { 24 | return service.hasPwd() 25 | } 26 | 27 | override fun setPwd(newPwd: String) { 28 | service.setPwd(newPwd.toSha256()) 29 | } 30 | 31 | override fun clearPwd() { 32 | service.clearPwd() 33 | } 34 | 35 | override fun authenticate(pwd: String): Boolean { 36 | return service.authenticate(pwd.toSha256()) 37 | } 38 | 39 | override fun agree(opId: Int) { 40 | service.agree(opId) 41 | } 42 | 43 | override fun prevent(opId: Int) { 44 | service.prevent(opId) 45 | } 46 | 47 | override fun isValid(opId: Int): Boolean { 48 | return service.isValid(opId) 49 | } 50 | 51 | override fun getAllOpRecord(): List { 52 | return service.allOpRecord ?: emptyList() 53 | } 54 | 55 | override fun clearAllOpRecord() { 56 | service.clearAllOpRecord() 57 | } 58 | 59 | @OptIn(ExperimentalStdlibApi::class) 60 | private fun String.toSha256(): String { 61 | if (isBlank()) { 62 | return "" 63 | } 64 | val msgDigest = MessageDigest.getInstance("SHA-256") 65 | return runCatching { 66 | msgDigest.update(toByteArray()) 67 | msgDigest.digest().toHexString() 68 | }.getOrElse { "" } 69 | } 70 | 71 | companion object { 72 | private const val TAG = "AuthClient" 73 | private const val KEY_AUTH = "key_auth" 74 | 75 | private const val KEY_AUTH_DATA = "key_auth_data" 76 | 77 | private var client: AuthClient? = null 78 | 79 | private val Dummy = AuthClient(IAuth.Default()) 80 | 81 | val AuthClient.isDummy get() = this === Dummy 82 | 83 | operator fun invoke(): AuthClient { 84 | return client ?: Dummy 85 | } 86 | 87 | fun inject(intent: Intent) { 88 | val binder = intent.extras?.getBinder(KEY_AUTH) 89 | Log.d(TAG, "$binder") 90 | if (binder != null) { 91 | client = AuthClient(IAuth.Stub.asInterface(binder)) 92 | } 93 | } 94 | 95 | fun parseAuthData(intent: Intent): AuthData { 96 | return intent.getParcelableExtra(KEY_AUTH_DATA) ?: AuthData.Empty 97 | } 98 | 99 | fun buildAuthIntent(remote: IAuth.Stub, authData: AuthData): Intent { 100 | val component = ComponentName(BuildConfig.APPLICATION_ID, AuthActivity::class.java.name) 101 | val bundle = Bundle().apply { 102 | putBinder(KEY_AUTH, remote.asBinder()) 103 | putParcelable(KEY_AUTH_DATA, authData) 104 | } 105 | return Intent().apply { 106 | setComponent(component) 107 | putExtras(bundle) 108 | addFlags( 109 | Intent.FLAG_ACTIVITY_NEW_TASK 110 | ) 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/auth/entities/AuthData.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.auth.entities 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.os.Parcelable 5 | import androidx.annotation.Keep 6 | import kotlinx.parcelize.Parcelize 7 | 8 | @Keep 9 | @Parcelize 10 | data class AuthData( 11 | val opId: Int, 12 | val opTypeOrdinal: Int, 13 | val opUid: Int, 14 | val opAppInfo: ApplicationInfo?, 15 | val appInfo: ApplicationInfo 16 | ) : Parcelable { 17 | 18 | val opType get() = OpType.entries[opTypeOrdinal] 19 | 20 | val isEmpty get() = this === Empty 21 | 22 | companion object { 23 | val Empty = AuthData( 24 | -1, 25 | -1, 26 | -1, 27 | null, 28 | ApplicationInfo() 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/auth/entities/OpRecord.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.auth.entities 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.Keep 5 | import cn.tinyhai.ban_uninstall.transact.entities.PkgInfo 6 | import kotlinx.parcelize.Parcelize 7 | 8 | @Keep 9 | @Parcelize 10 | data class OpRecord( 11 | val label: String?, 12 | val opTypeOrdinal: Int, 13 | val pkgInfoString: String, 14 | val callingUid: Int, 15 | val callingPackageName: String, 16 | val resultOrdinal: Int, 17 | val timeMillis: Long, 18 | ) : Parcelable { 19 | 20 | val opType get() = OpType.entries[opTypeOrdinal] 21 | 22 | val result get() = OpResult.entries[resultOrdinal] 23 | 24 | val pkgInfo get() = PkgInfo(pkgInfoString) 25 | 26 | companion object { 27 | operator fun invoke( 28 | label: String?, 29 | opType: OpType, 30 | pkgInfo: PkgInfo, 31 | callingUid: Int, 32 | callingPackageName: String, 33 | result: OpResult = OpResult.Unhandled 34 | ): OpRecord { 35 | return OpRecord( 36 | label, 37 | opType.ordinal, 38 | pkgInfo.toString(), 39 | callingUid, 40 | callingPackageName, 41 | result.ordinal, 42 | System.currentTimeMillis() 43 | ) 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/auth/entities/OpResult.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.auth.entities 2 | 3 | enum class OpResult { 4 | Unhandled, 5 | Allowed, 6 | Prevented 7 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/auth/entities/OpType.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.auth.entities 2 | 3 | enum class OpType { 4 | ClearData, 5 | Uninstall 6 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/auth/server/AuthHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.auth.server 2 | 3 | import cn.tinyhai.ban_uninstall.BuildConfig 4 | import cn.tinyhai.ban_uninstall.configs.Configs 5 | import cn.tinyhai.ban_uninstall.utils.writeWithBak 6 | import java.io.File 7 | 8 | class AuthHelper { 9 | 10 | private val authFile: File 11 | get() = File(Configs.authFilePath).apply { 12 | parentFile?.mkdirs() 13 | } 14 | 15 | private var sha256 = if (authFile.exists()) authFile.readText().trim() else "" 16 | set(value) { 17 | if (field != value) { 18 | field = value 19 | store(value) 20 | } 21 | } 22 | 23 | val hasPwd get() = sha256.isNotBlank() 24 | 25 | fun authenticate(sha256: String): Boolean { 26 | if (!hasPwd) { 27 | return true 28 | } 29 | if (sha256.isBlank()) { 30 | return false 31 | } 32 | return sha256 == this.sha256 33 | } 34 | 35 | fun setPwd(newSha256: String) { 36 | this.sha256 = newSha256 37 | } 38 | 39 | fun clearAuth() { 40 | sha256 = "" 41 | deleteAuthFile() 42 | } 43 | 44 | private fun deleteAuthFile() { 45 | authFile.delete() 46 | } 47 | 48 | private fun store(sha256: String) { 49 | authFile.writeWithBak { 50 | append(sha256) 51 | flush() 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/auth/server/AuthService.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.auth.server 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.os.Binder 5 | import cn.tinyhai.ban_uninstall.auth.IAuth 6 | import cn.tinyhai.ban_uninstall.auth.client.AuthClient 7 | import cn.tinyhai.ban_uninstall.auth.entities.AuthData 8 | import cn.tinyhai.ban_uninstall.auth.entities.OpRecord 9 | import cn.tinyhai.ban_uninstall.auth.entities.OpResult 10 | import cn.tinyhai.ban_uninstall.auth.entities.OpType 11 | import cn.tinyhai.ban_uninstall.transact.entities.PkgInfo 12 | import cn.tinyhai.ban_uninstall.transact.server.TransactService 13 | import cn.tinyhai.ban_uninstall.utils.SystemContextHolder 14 | import cn.tinyhai.ban_uninstall.utils.XPLogUtils 15 | 16 | object AuthService : IAuth.Stub() { 17 | 18 | private val helper = AuthHelper() 19 | 20 | private val pendingOp = PendingOpList() 21 | 22 | private val opRecordList = OpRecordList() 23 | 24 | override fun hasPwd(): Boolean { 25 | return helper.hasPwd 26 | } 27 | 28 | override fun setPwd(newSha256: String) { 29 | helper.setPwd(newSha256) 30 | } 31 | 32 | override fun clearPwd() { 33 | helper.clearAuth() 34 | } 35 | 36 | override fun authenticate(sha256: String): Boolean { 37 | return helper.authenticate(sha256) 38 | } 39 | 40 | fun showClearDataConfirm( 41 | onConfirm: () -> Unit, 42 | onCancel: () -> Unit, 43 | pkgInfo: PkgInfo, 44 | callingUid: Int, 45 | callingPackageName: String 46 | ) { 47 | val appInfo = 48 | TransactService.getApplicationInfoAsUser(pkgInfo.packageName, pkgInfo.userId) 49 | val opId = SystemContextHolder.withSystemContext { 50 | pendingOp.add( 51 | wrapWithPendingOp( 52 | OpRecord( 53 | label = appInfo?.loadLabel(packageManager)?.toString(), 54 | opType = OpType.ClearData, 55 | pkgInfo, 56 | callingUid, 57 | callingPackageName 58 | ), 59 | onConfirm, 60 | onCancel 61 | ) 62 | ) 63 | } 64 | 65 | if (appInfo == null) { 66 | XPLogUtils.log("maybe there is a sharedlibrary is being uninstalled, just skip it") 67 | agree(opId) 68 | return 69 | } 70 | 71 | runCatching { 72 | val callingAppInfo = 73 | TransactService.getApplicationInfoAsUser(callingPackageName, callingUid / 100_000) 74 | startAuthActivity(opId, OpType.ClearData, appInfo, callingUid, callingAppInfo) 75 | }.onSuccess { 76 | if (!it) { 77 | prevent(opId) 78 | } 79 | }.onFailure { 80 | XPLogUtils.log(it) 81 | XPLogUtils.log("!!!! what's wrong?") 82 | agree(opId) 83 | } 84 | } 85 | 86 | fun showUninstallConfirm( 87 | onConfirm: () -> Unit, 88 | onCancel: () -> Unit, 89 | pkgInfo: PkgInfo, 90 | callingUid: Int, 91 | callingPackageName: String 92 | ) { 93 | val appInfo = 94 | TransactService.getApplicationInfoAsUser(pkgInfo.packageName, pkgInfo.userId) 95 | val opId = SystemContextHolder.withSystemContext { 96 | pendingOp.add( 97 | wrapWithPendingOp( 98 | OpRecord( 99 | label = appInfo?.loadLabel(packageManager)?.toString(), 100 | opType = OpType.Uninstall, 101 | pkgInfo, 102 | callingUid, 103 | callingPackageName 104 | ), 105 | onConfirm, 106 | onCancel 107 | ) 108 | ) 109 | } 110 | 111 | if (appInfo == null) { 112 | XPLogUtils.log("!!!! $pkgInfo not found") 113 | agree(opId) 114 | return 115 | } 116 | 117 | runCatching { 118 | val callingAppInfo = 119 | TransactService.getApplicationInfoAsUser(callingPackageName, callingUid / 100_000) 120 | startAuthActivity(opId, OpType.Uninstall, appInfo, callingUid, callingAppInfo) 121 | }.onSuccess { 122 | if (!it) { 123 | prevent(opId) 124 | } 125 | }.onFailure { 126 | XPLogUtils.log(it) 127 | XPLogUtils.log("!!!! what's wrong?") 128 | agree(opId) 129 | } 130 | } 131 | 132 | fun onUninstall( 133 | pkgInfo: PkgInfo, 134 | callingUid: Int, 135 | callingPackageName: String, 136 | result: OpResult 137 | ) { 138 | SystemContextHolder.withSystemContext { 139 | val appInfo = 140 | TransactService.getApplicationInfoAsUser(pkgInfo.packageName, pkgInfo.userId) 141 | opRecordList.add( 142 | opRecord = OpRecord( 143 | label = appInfo?.loadLabel(packageManager)?.toString(), 144 | opType = OpType.Uninstall, 145 | pkgInfo, 146 | callingUid, 147 | callingPackageName 148 | ), 149 | result = result 150 | ) 151 | } 152 | } 153 | 154 | fun onClearData( 155 | pkgInfo: PkgInfo, 156 | callingUid: Int, 157 | callingPackageName: String, 158 | result: OpResult 159 | ) { 160 | SystemContextHolder.withSystemContext { 161 | val appInfo = 162 | TransactService.getApplicationInfoAsUser(pkgInfo.packageName, pkgInfo.userId) 163 | opRecordList.add( 164 | opRecord = OpRecord( 165 | label = appInfo?.loadLabel(packageManager)?.toString(), 166 | opType = OpType.ClearData, 167 | pkgInfo, 168 | callingUid, 169 | callingPackageName 170 | ), 171 | result = result 172 | ) 173 | } 174 | } 175 | 176 | private fun startAuthActivity( 177 | opId: Int, 178 | opType: OpType, 179 | appInfo: ApplicationInfo, 180 | callingUid: Int, 181 | callingAppInfo: ApplicationInfo?, 182 | ): Boolean { 183 | var success = false 184 | val ident = Binder.clearCallingIdentity() 185 | try { 186 | SystemContextHolder.withSystemContext { 187 | val authData = AuthData( 188 | opId = opId, 189 | opTypeOrdinal = opType.ordinal, 190 | opUid = callingUid, 191 | opAppInfo = callingAppInfo, 192 | appInfo = appInfo 193 | ) 194 | val intent = AuthClient.buildAuthIntent(AuthService, authData) 195 | if (packageManager.resolveActivity(intent, 0) != null) { 196 | startActivity(intent) 197 | success = true 198 | } 199 | } 200 | } finally { 201 | Binder.restoreCallingIdentity(ident) 202 | } 203 | return success 204 | } 205 | 206 | private fun wrapWithPendingOp( 207 | opRecord: OpRecord, 208 | onAgree: () -> Unit, 209 | onPrevent: () -> Unit 210 | ): PendingOpList.PendingOp { 211 | return object : PendingOpList.PendingOp { 212 | override fun agree() { 213 | onAgree() 214 | opRecordList.add(opRecord, OpResult.Allowed) 215 | } 216 | 217 | override fun prevent() { 218 | onPrevent() 219 | opRecordList.add(opRecord, OpResult.Prevented) 220 | } 221 | } 222 | } 223 | 224 | override fun agree(opId: Int) { 225 | val ident = Binder.clearCallingIdentity() 226 | try { 227 | pendingOp.remove(opId)?.agree() 228 | } finally { 229 | Binder.restoreCallingIdentity(ident) 230 | } 231 | } 232 | 233 | override fun prevent(opId: Int) { 234 | val ident = Binder.clearCallingIdentity() 235 | try { 236 | pendingOp.remove(opId)?.prevent() 237 | } finally { 238 | Binder.restoreCallingIdentity(ident) 239 | } 240 | } 241 | 242 | fun preventAll() { 243 | val ident = Binder.clearCallingIdentity() 244 | try { 245 | pendingOp.removeAll().forEach { 246 | it.prevent() 247 | } 248 | } finally { 249 | Binder.restoreCallingIdentity(ident) 250 | } 251 | } 252 | 253 | override fun isValid(opId: Int): Boolean { 254 | return pendingOp.contains(opId) 255 | } 256 | 257 | override fun getAllOpRecord(): List { 258 | return opRecordList.toList() 259 | } 260 | 261 | override fun clearAllOpRecord() { 262 | opRecordList.clear() 263 | } 264 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/auth/server/OpRecordList.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.auth.server 2 | 3 | import cn.tinyhai.ban_uninstall.auth.entities.OpRecord 4 | import cn.tinyhai.ban_uninstall.auth.entities.OpResult 5 | 6 | class OpRecordList { 7 | 8 | private val records: MutableList = ArrayList() 9 | 10 | fun add(opRecord: OpRecord, result: OpResult) { 11 | synchronized(records) { 12 | records.add(opRecord.copy(resultOrdinal = result.ordinal)) 13 | } 14 | } 15 | 16 | fun clear() { 17 | synchronized(records) { 18 | records.clear() 19 | } 20 | } 21 | 22 | fun toList(): List { 23 | return synchronized(records) { 24 | records.toList() 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/auth/server/PendingOpList.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.auth.server 2 | 3 | import android.util.SparseArray 4 | import androidx.core.util.valueIterator 5 | 6 | class PendingOpList { 7 | private var counter = 0 8 | 9 | private val pending = SparseArray() 10 | 11 | fun add(op: PendingOp): Int { 12 | return synchronized(pending) { 13 | val opId = counter.also { counter += 1 } 14 | pending.put(opId, op) 15 | opId 16 | } 17 | } 18 | 19 | fun contains(opId: Int): Boolean { 20 | return synchronized(pending) { 21 | pending[opId] != null 22 | } 23 | } 24 | 25 | fun remove(opId: Int): PendingOp? { 26 | return synchronized(pending) { 27 | pending[opId]?.also { pending.remove(opId) } 28 | } 29 | } 30 | 31 | fun removeAll(): List { 32 | return synchronized(pending) { 33 | Iterable { pending.valueIterator() }.toList().also { 34 | pending.clear() 35 | } 36 | } 37 | } 38 | 39 | interface PendingOp { 40 | fun agree() 41 | fun prevent() 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/configs/Configs.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.configs 2 | 3 | import cn.tinyhai.ban_uninstall.BuildConfig 4 | import cn.tinyhai.ban_uninstall.utils.XPLogUtils 5 | import java.io.File 6 | 7 | object Configs { 8 | private const val CONFIG_PATH = "/data/misc/adb/${BuildConfig.APPLICATION_ID}" 9 | private const val AUTH_FILE_PATH = "$CONFIG_PATH/auth" 10 | private const val BANNED_PKG_LIST_FILE_PATH = "${CONFIG_PATH}/banned_pkg_list" 11 | 12 | val authFilePath = AUTH_FILE_PATH 13 | val bannedPkgListFilePath = BANNED_PKG_LIST_FILE_PATH 14 | 15 | fun onSelfRemoved() { 16 | destroy() 17 | } 18 | 19 | private fun destroy() { 20 | XPLogUtils.log("delete $CONFIG_PATH") 21 | File(CONFIG_PATH).let { 22 | if (it.exists()) { 23 | it.deleteRecursively() 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/hooker/Constants.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.hooker 2 | 3 | const val PKG_ANDROID = "android" 4 | const val PKG_SELF = "cn.tinyhai.ban_uninstall" -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/hooker/HookGetSystemContext.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.hooker 2 | 3 | import android.content.Context 4 | import cn.tinyhai.ban_uninstall.XposedInit 5 | import cn.tinyhai.ban_uninstall.receiver.PackageChangeReceiver 6 | import cn.tinyhai.ban_uninstall.receiver.RestartMainReceiver 7 | import cn.tinyhai.ban_uninstall.transact.entities.ActiveMode 8 | import cn.tinyhai.ban_uninstall.transact.server.TransactService 9 | import cn.tinyhai.ban_uninstall.utils.SystemContextHolder 10 | import cn.tinyhai.xp.annotation.* 11 | import cn.tinyhai.xp.hook.logger.XPLogger 12 | import de.robv.android.xposed.XC_MethodHook 13 | import de.robv.android.xposed.XposedHelpers 14 | import de.robv.android.xposed.callbacks.XC_LoadPackage 15 | 16 | @HookScope( 17 | targetPackageName = PKG_ANDROID, 18 | targetProcessName = PKG_ANDROID 19 | ) 20 | class HookGetSystemContext( 21 | private val logger: XPLogger 22 | ) { 23 | companion object { 24 | private const val ID_SYSTEM_SERVICE_MANAGER = "id_system_service_manager" 25 | } 26 | 27 | private lateinit var lp: XC_LoadPackage.LoadPackageParam 28 | 29 | @Initiate 30 | fun initiate(lp: XC_LoadPackage.LoadPackageParam) { 31 | this.lp = lp 32 | } 33 | 34 | @HookerGate 35 | fun hookerGate(hookerId: String): Boolean { 36 | return when (hookerId) { 37 | ID_SYSTEM_SERVICE_MANAGER -> shouldHookSystemServiceManager() 38 | else -> false 39 | } 40 | } 41 | 42 | private fun shouldHookSystemServiceManager(): Boolean { 43 | if (XposedInit.activeMode == ActiveMode.Xposed) { 44 | return true 45 | } 46 | 47 | onBootComplete() 48 | 49 | return false 50 | } 51 | 52 | @Oneshot(unhookable = true) 53 | @HookerId(ID_SYSTEM_SERVICE_MANAGER) 54 | @MethodHooker( 55 | className = "com.android.server.SystemServiceManager", 56 | methodName = "startBootPhase", 57 | hookType = HookType.AfterMethod 58 | ) 59 | fun afterStartBootPhase(param: XC_MethodHook.MethodHookParam, unhook: () -> Unit) { 60 | val phase = param.args.lastOrNull() as? Int ?: 0 61 | if (phase == 1000 /* PHASE_BOOT_COMPLETED */) { 62 | onBootComplete() 63 | unhook() 64 | } 65 | } 66 | 67 | private fun onBootComplete() { 68 | TransactService.onSystemBootCompleted() 69 | val activityThread = 70 | XposedHelpers.findClass("android.app.ActivityThread", lp.classLoader) 71 | .getDeclaredField("sCurrentActivityThread") 72 | .also { it.isAccessible = true }.get(null) 73 | val systemContext = 74 | activityThread::class.java.getDeclaredField("mSystemContext") 75 | .also { it.isAccessible = true }.get(activityThread) as? Context 76 | logger.info("systemContext: $systemContext") 77 | systemContext?.let { 78 | SystemContextHolder.onSystemContext(it) 79 | PackageChangeReceiver().register(it) 80 | if (XposedInit.activeMode == ActiveMode.Root) { 81 | RestartMainReceiver.send(it) 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/hooker/HookInjectSelf.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.hooker 2 | 3 | import android.content.Intent 4 | import android.content.pm.ActivityInfo 5 | import android.os.Build 6 | import cn.tinyhai.ban_uninstall.transact.client.TransactClient 7 | import cn.tinyhai.ban_uninstall.transact.server.TransactService 8 | import cn.tinyhai.xp.annotation.HookScope 9 | import cn.tinyhai.xp.annotation.HookType 10 | import cn.tinyhai.xp.annotation.MethodHooker 11 | import de.robv.android.xposed.XC_MethodHook 12 | 13 | @HookScope( 14 | targetPackageName = PKG_ANDROID, 15 | targetProcessName = PKG_ANDROID 16 | ) 17 | class HookInjectSelf { 18 | 19 | @MethodHooker( 20 | className = "android.app.servertransaction.LaunchActivityItem", 21 | methodName = "obtain", 22 | hookType = HookType.BeforeMethod, 23 | minSdkInclusive = Build.VERSION_CODES.P 24 | ) 25 | fun beforeObtain(param: XC_MethodHook.MethodHookParam) { 26 | handleLaunchActivity(param) 27 | } 28 | 29 | @MethodHooker( 30 | className = "android.app.IApplicationThread\$Stub\$Proxy", 31 | methodName = "scheduleLaunchActivity", 32 | hookType = HookType.BeforeMethod, 33 | minSdkInclusive = Build.VERSION_CODES.O, 34 | maxSdkExclusive = Build.VERSION_CODES.P 35 | ) 36 | fun beforeScheduleLaunchActivity0(param: XC_MethodHook.MethodHookParam) { 37 | handleLaunchActivity(param) 38 | } 39 | 40 | @MethodHooker( 41 | className = "android.app.ApplicationThreadNative", 42 | methodName = "scheduleLaunchActivity", 43 | hookType = HookType.BeforeMethod, 44 | maxSdkExclusive = Build.VERSION_CODES.O 45 | ) 46 | fun beforeScheduleLaunchActivity1(param: XC_MethodHook.MethodHookParam) { 47 | handleLaunchActivity(param) 48 | } 49 | 50 | private fun handleLaunchActivity(param: XC_MethodHook.MethodHookParam) { 51 | val intent = param.args.firstOrNull { it is Intent } as? Intent ?: return 52 | val aInfo = param.args.firstOrNull { it is ActivityInfo } as? ActivityInfo ?: return 53 | val userId = aInfo.applicationInfo.uid / 100_000 54 | TransactClient.injectBinderIfNeeded(TransactService, intent, userId) 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/hooker/HookSelf.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.hooker 2 | 3 | import cn.tinyhai.xp.annotation.* 4 | import cn.tinyhai.xp.hook.logger.XPLogger 5 | import de.robv.android.xposed.XC_MethodHook.MethodHookParam 6 | import de.robv.android.xposed.XposedBridge 7 | import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam 8 | 9 | @HookScope( 10 | PKG_SELF, 11 | PKG_SELF 12 | ) 13 | class HookSelf( 14 | private val logger: XPLogger 15 | ) { 16 | 17 | companion object { 18 | private const val ID_CHECK_MODE = "check_mode" 19 | } 20 | 21 | private var skip = false 22 | 23 | @Initiate 24 | fun init(lp: LoadPackageParam) { 25 | skip = lp.appInfo.uid / 100_000 > 0 26 | if (skip) { 27 | logger.info("skip dual app") 28 | } 29 | } 30 | 31 | @HookerGate 32 | fun isHookerEnable(id: String): Boolean { 33 | return !skip && when (id) { 34 | ID_CHECK_MODE -> XposedBridge.getXposedVersion() <= 92 35 | else -> false 36 | } 37 | } 38 | 39 | @HookerId(ID_CHECK_MODE) 40 | @MethodHooker( 41 | className = "android.app.ContextImpl", 42 | methodName = "checkMode", 43 | hookType = HookType.AfterMethod 44 | ) 45 | fun afterCheckMode(param: MethodHookParam) { 46 | if (param.args[0] as Int and 0x0001 /* Context.MODE_WORLD_READABLE */ != 0) { 47 | param.throwable = null 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/receiver/BootCompletedReceiver.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | import cn.tinyhai.ban_uninstall.utils.tryToInjectIntoSystemServer 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.launch 11 | 12 | class BootCompletedReceiver : BroadcastReceiver() { 13 | 14 | companion object { 15 | private const val TAG = "BootCompletedReceiver" 16 | private const val ACTION = Intent.ACTION_BOOT_COMPLETED 17 | } 18 | 19 | override fun onReceive(context: Context, intent: Intent) { 20 | if (intent.action == ACTION) { 21 | Log.d(TAG, "BootCompleted") 22 | CoroutineScope(Dispatchers.Unconfined).launch { 23 | tryToInjectIntoSystemServer() 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/receiver/PackageChangeReceiver.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.receiver 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.IntentFilter 8 | import android.os.Handler 9 | import androidx.core.content.ContextCompat 10 | import cn.tinyhai.ban_uninstall.BuildConfig 11 | import cn.tinyhai.ban_uninstall.auth.server.AuthService 12 | import cn.tinyhai.ban_uninstall.configs.Configs 13 | import cn.tinyhai.ban_uninstall.transact.server.TransactService 14 | import cn.tinyhai.ban_uninstall.utils.XPLogUtils 15 | 16 | @SuppressLint("PrivateApi") 17 | class PackageChangeReceiver : BroadcastReceiver() { 18 | 19 | private val intentFilter = IntentFilter().apply { 20 | addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED) 21 | addAction(Intent.ACTION_PACKAGE_REPLACED) 22 | addDataScheme("package") 23 | } 24 | 25 | private val registerReceiverForAllUsers by lazy { 26 | try { 27 | Context::class.java.getDeclaredMethod( 28 | "registerReceiverForAllUsers", 29 | BroadcastReceiver::class.java, 30 | IntentFilter::class.java, 31 | String::class.java, 32 | Handler::class.java 33 | ) 34 | } catch (e: NoSuchMethodException) { 35 | null 36 | } 37 | } 38 | 39 | private val getSendingUserId by lazy { 40 | BroadcastReceiver::class.java.getDeclaredMethod("getSendingUserId") 41 | .also { it.isAccessible = true } 42 | } 43 | 44 | override fun onReceive(context: Context, intent: Intent) { 45 | if (!intentFilter.hasAction(intent.action)) { 46 | return 47 | } 48 | 49 | val sendingUserId = getSendingUserId.invoke(this) as Int 50 | val uri = intent.data 51 | val packageName = uri?.encodedSchemeSpecificPart 52 | when (intent.action) { 53 | Intent.ACTION_PACKAGE_REPLACED -> { 54 | XPLogUtils.log("pkg replace uri = $uri, userId = $sendingUserId") 55 | if (packageName == BuildConfig.APPLICATION_ID && sendingUserId == 0) { 56 | AuthService.preventAll() 57 | } 58 | } 59 | 60 | Intent.ACTION_PACKAGE_FULLY_REMOVED -> { 61 | XPLogUtils.log("pkg uninstall uri = $uri, userId = $sendingUserId") 62 | packageName?.let { 63 | TransactService.onPkgUninstall(packageName, sendingUserId) 64 | } 65 | if (packageName == BuildConfig.APPLICATION_ID && sendingUserId == 0) { 66 | XPLogUtils.log("self package removed") 67 | Configs.onSelfRemoved() 68 | } 69 | } 70 | } 71 | } 72 | 73 | fun register(context: Context) { 74 | XPLogUtils.log("register PackageChangeReceiver") 75 | registerReceiverForAllUsers?.let { 76 | it.invoke( 77 | context, 78 | this, 79 | intentFilter, 80 | null, 81 | null 82 | ) 83 | Unit 84 | } ?: ContextCompat.registerReceiver( 85 | context, this, intentFilter, 86 | ContextCompat.RECEIVER_NOT_EXPORTED 87 | ) 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/receiver/RestartMainReceiver.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import android.util.Log 8 | import androidx.core.content.ContextCompat 9 | import cn.tinyhai.ban_uninstall.BuildConfig 10 | import cn.tinyhai.ban_uninstall.MainActivity 11 | 12 | class RestartMainReceiver : BroadcastReceiver() { 13 | companion object { 14 | private const val TAG = "RestartMainReceiver" 15 | private const val ACTION = "${BuildConfig.APPLICATION_ID}.RESTART_MAIN" 16 | 17 | fun send(context: Context) { 18 | val intent = Intent().apply { 19 | setAction(ACTION) 20 | } 21 | context.sendBroadcast(intent) 22 | } 23 | 24 | fun register(context: Context): () -> Unit { 25 | val receiver = RestartMainReceiver() 26 | ContextCompat.registerReceiver( 27 | context, 28 | receiver, 29 | IntentFilter(ACTION), 30 | ContextCompat.RECEIVER_NOT_EXPORTED 31 | ) 32 | return { 33 | context.unregisterReceiver(receiver) 34 | } 35 | } 36 | } 37 | 38 | override fun onReceive(context: Context, intent: Intent) { 39 | if (intent.action == ACTION) { 40 | Log.d(TAG, "restart") 41 | MainActivity.restart() 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/transact/client/TransactClient.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.transact.client 2 | 3 | import android.content.ComponentName 4 | import android.content.Intent 5 | import android.content.pm.ApplicationInfo 6 | import android.content.pm.PackageInfo 7 | import android.os.Bundle 8 | import android.os.IBinder 9 | import android.util.Log 10 | import cn.tinyhai.ban_uninstall.BuildConfig 11 | import cn.tinyhai.ban_uninstall.MainActivity 12 | import cn.tinyhai.ban_uninstall.transact.ITransactor 13 | import cn.tinyhai.ban_uninstall.utils.XPLogUtils 14 | import rikka.parcelablelist.ParcelableListSlice 15 | 16 | class TransactClient( 17 | private var service: ITransactor 18 | ) : ITransactor { 19 | 20 | init { 21 | service.onAppLaunched() 22 | } 23 | 24 | override fun getPackages(): ParcelableListSlice { 25 | return service.packages ?: ParcelableListSlice(emptyList()) 26 | } 27 | 28 | override fun banPackage( 29 | packageNames: List, 30 | bannedPackages: MutableList 31 | ) { 32 | service.banPackage(packageNames, bannedPackages) 33 | } 34 | 35 | override fun freePackage( 36 | packageNames: List, 37 | freedPackages: MutableList 38 | ) { 39 | service.freePackage(packageNames, freedPackages) 40 | } 41 | 42 | override fun asBinder(): IBinder? { 43 | return service.asBinder() 44 | } 45 | 46 | override fun getAllBannedPackages(): List { 47 | return service.allBannedPackages ?: emptyList() 48 | } 49 | 50 | override fun getAuth(): IBinder? { 51 | return service.auth 52 | } 53 | 54 | override fun getActiveMode(): Int { 55 | return service.activeMode 56 | } 57 | 58 | override fun syncPrefs(prefs: Map?): Boolean { 59 | return service.syncPrefs(prefs) 60 | } 61 | 62 | override fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo? { 63 | return service.getApplicationInfoAsUser(packageName, userId) 64 | } 65 | 66 | override fun sayHello(hello: String): String { 67 | return service.sayHello(hello) ?: "" 68 | } 69 | 70 | override fun onAppLaunched() { 71 | service.onAppLaunched() 72 | } 73 | 74 | override fun reloadPrefs() { 75 | service.reloadPrefs() 76 | } 77 | 78 | companion object { 79 | private const val TAG = "TransactClient" 80 | 81 | private const val KEY_TRANSACT = "key_transact" 82 | 83 | private var client: TransactClient? = null 84 | 85 | private val Dummy = TransactClient(ITransactor.Default()) 86 | 87 | operator fun invoke(): TransactClient { 88 | return client ?: Dummy 89 | } 90 | 91 | fun inject(intent: Intent) { 92 | val binder = intent.extras?.getBinder(KEY_TRANSACT) 93 | Log.d(TAG, "$binder") 94 | if (binder != null) { 95 | client = TransactClient(ITransactor.Stub.asInterface(binder)) 96 | } 97 | } 98 | 99 | private fun ComponentName.isSelf(): Boolean { 100 | return packageName == BuildConfig.APPLICATION_ID && className == MainActivity::class.qualifiedName 101 | } 102 | 103 | fun injectBinderIfNeeded(service: ITransactor.Stub, intent: Intent, userId: Int) { 104 | val component = intent.component ?: return 105 | if (component.isSelf()) { 106 | if (userId > 0) { 107 | XPLogUtils.log("inject skip dual app") 108 | return 109 | } 110 | val bundle = Bundle().apply { 111 | putBinder(KEY_TRANSACT, service.asBinder()) 112 | } 113 | intent.apply { 114 | putExtras(bundle) 115 | } 116 | XPLogUtils.log("inject transact success") 117 | } 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/transact/entities/ActiveMode.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.transact.entities 2 | 3 | enum class ActiveMode(val description: String) { 4 | Disabled("Disabled"), 5 | Xposed("Xposed"), 6 | Root("Root(LSPatch)") 7 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/transact/entities/PkgInfo.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.transact.entities 2 | 3 | data class PkgInfo( 4 | val packageName: String, 5 | val userId: Int 6 | ) { 7 | companion object { 8 | operator fun invoke(packageNameWithUserId: String): PkgInfo { 9 | val splitCharIndex = packageNameWithUserId.indexOf(":") 10 | val (packageName, userId) = if (splitCharIndex > 0) { 11 | packageNameWithUserId.substring( 12 | 0, 13 | endIndex = splitCharIndex 14 | ) to packageNameWithUserId.substring(splitCharIndex + 1).toInt() 15 | } else { 16 | throw IllegalArgumentException("packageNameWithUserId: $packageNameWithUserId userId not found") 17 | } 18 | return PkgInfo(packageName, userId) 19 | } 20 | } 21 | 22 | override fun toString(): String { 23 | return "$packageName:$userId" 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/transact/server/BannedPkgHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.transact.server 2 | 3 | import cn.tinyhai.ban_uninstall.configs.Configs 4 | import cn.tinyhai.ban_uninstall.transact.entities.PkgInfo 5 | import cn.tinyhai.ban_uninstall.utils.XPLogUtils 6 | import cn.tinyhai.ban_uninstall.utils.writeWithBak 7 | import java.io.File 8 | 9 | class BannedPkgHelper { 10 | 11 | private val ioLock = Any() 12 | 13 | private val bannedPkgListFile: File 14 | get() = File(Configs.bannedPkgListFilePath).apply { 15 | parentFile?.mkdirs() 16 | } 17 | 18 | private val bannedPkgSet = HashSet() 19 | 20 | val allBannedPackages 21 | get() = synchronized(bannedPkgSet) { 22 | bannedPkgSet.map { it.toString() }.toList() 23 | } 24 | 25 | val allBannedPkgInfo 26 | get() = synchronized(bannedPkgSet) { 27 | bannedPkgSet.toSet() 28 | } 29 | 30 | fun loadBannedPkgList() { 31 | val hashSet = HashSet() 32 | synchronized(ioLock) { 33 | runCatching { 34 | bannedPkgListFile.bufferedReader().use { 35 | it.forEachLine { pkgWithUserId -> 36 | if (pkgWithUserId.isNotBlank()) { 37 | hashSet.add(PkgInfo(pkgWithUserId)) 38 | } 39 | } 40 | } 41 | }.onFailure { 42 | XPLogUtils.log(it) 43 | }.onSuccess { 44 | XPLogUtils.log("bannedPkgList loaded") 45 | } 46 | } 47 | synchronized(bannedPkgSet) { 48 | bannedPkgSet.clear() 49 | bannedPkgSet.addAll(hashSet) 50 | } 51 | } 52 | 53 | fun storeBannedPkgList() { 54 | synchronized(ioLock) { 55 | val list = allBannedPackages 56 | bannedPkgListFile.writeWithBak { 57 | list.forEach { pkg -> 58 | write(pkg) 59 | newLine() 60 | } 61 | }.onFailure { 62 | XPLogUtils.log("bannedPkgList save failed") 63 | }.onSuccess { 64 | XPLogUtils.log("bannedPkgList saved") 65 | } 66 | } 67 | } 68 | 69 | fun removePkgs(packageNames: List, removed: MutableList) { 70 | synchronized(bannedPkgSet) { 71 | packageNames.forEach { pkgWithUserId -> 72 | if (bannedPkgSet.remove(PkgInfo(pkgWithUserId))) { 73 | removed.add(pkgWithUserId) 74 | } 75 | } 76 | } 77 | } 78 | 79 | fun addPkgs(packageNames: List, added: MutableList) { 80 | synchronized(bannedPkgSet) { 81 | packageNames.forEach { pkgWithUserId -> 82 | if (bannedPkgSet.add(PkgInfo(pkgWithUserId))) { 83 | added.add(pkgWithUserId) 84 | } 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/transact/server/TransactService.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.transact.server 2 | 3 | import android.content.Context 4 | import android.content.pm.ApplicationInfo 5 | import android.content.pm.IPackageManager 6 | import android.content.pm.PackageInfo 7 | import android.content.pm.PackageManager.NameNotFoundException 8 | import android.os.* 9 | import cn.tinyhai.ban_uninstall.BuildConfig 10 | import cn.tinyhai.ban_uninstall.XposedInit 11 | import cn.tinyhai.ban_uninstall.auth.server.AuthService 12 | import cn.tinyhai.ban_uninstall.transact.ITransactor 13 | import cn.tinyhai.ban_uninstall.transact.entities.PkgInfo 14 | import cn.tinyhai.ban_uninstall.utils.HandlerUtils 15 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_FILE_NAME 16 | import cn.tinyhai.ban_uninstall.utils.XPLogUtils 17 | import cn.tinyhai.ban_uninstall.utils.XSharedPrefs 18 | import de.robv.android.xposed.XSharedPreferences 19 | import rikka.parcelablelist.ParcelableListSlice 20 | 21 | interface PkgInfoContainer { 22 | fun contains(packageName: String, userId: Int): Boolean 23 | } 24 | 25 | object TransactService : ITransactor.Stub(), PkgInfoContainer { 26 | 27 | private val helper = BannedPkgHelper() 28 | 29 | private lateinit var pm: IPackageManager 30 | private lateinit var um: IUserManager 31 | 32 | private const val STORE_BANNED_PKG_LIST_DELAY = 30_000L 33 | private val storeBannedPkgListJob = Runnable { 34 | helper.storeBannedPkgList() 35 | } 36 | 37 | fun onSystemBootCompleted() { 38 | pm = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) 39 | um = IUserManager.Stub.asInterface(ServiceManager.getService(Context.USER_SERVICE)) 40 | HandlerUtils.postWorker { 41 | helper.loadBannedPkgList() 42 | trimBannedPkgInfo { 43 | try { 44 | pm.getPackageInfo(it.packageName, 0, it.userId) 45 | false 46 | } catch (e: NameNotFoundException) { 47 | true 48 | } catch (th: Throwable) { 49 | XPLogUtils.log(th) 50 | true 51 | } 52 | } 53 | } 54 | } 55 | 56 | private fun postStoreJob() { 57 | HandlerUtils.removeWorkerRunnable(storeBannedPkgListJob) 58 | HandlerUtils.postWorkerDelay(storeBannedPkgListJob, STORE_BANNED_PKG_LIST_DELAY) 59 | } 60 | 61 | override fun getPackages(): ParcelableListSlice { 62 | val ident = Binder.clearCallingIdentity() 63 | try { 64 | val list = arrayListOf() 65 | val userIds = um.getProfileIds(Process.myUid() / 100_000, true) 66 | for (userId in userIds) { 67 | pm.getInstalledPackages(0, userId).list.filter { 68 | it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0 69 | }.let { 70 | list.addAll(it) 71 | } 72 | } 73 | return ParcelableListSlice(list) 74 | } finally { 75 | Binder.restoreCallingIdentity(ident) 76 | } 77 | } 78 | 79 | override fun banPackage( 80 | packageNames: List, 81 | bannedPackages: MutableList 82 | ) { 83 | helper.addPkgs(packageNames, bannedPackages) 84 | if (bannedPackages.isNotEmpty()) { 85 | bannedPackages.forEach { 86 | XPLogUtils.log("ban pkg $it") 87 | } 88 | postStoreJob() 89 | } 90 | } 91 | 92 | override fun freePackage( 93 | packageNames: List, 94 | freedPackages: MutableList 95 | ) { 96 | helper.removePkgs(packageNames, freedPackages) 97 | if (freedPackages.isNotEmpty()) { 98 | freedPackages.forEach { 99 | XPLogUtils.log("free pkg $it") 100 | } 101 | postStoreJob() 102 | } 103 | } 104 | 105 | override fun getAllBannedPackages(): List { 106 | return helper.allBannedPackages 107 | } 108 | 109 | override fun sayHello(hello: String): String { 110 | return "$hello from server" 111 | } 112 | 113 | override fun onAppLaunched() { 114 | XPLogUtils.log("App Launched") 115 | } 116 | 117 | override fun reloadPrefs() { 118 | XSharedPrefs.update(XSharedPreferences(BuildConfig.APPLICATION_ID, SP_FILE_NAME).all) 119 | } 120 | 121 | override fun getAuth(): IBinder { 122 | return AuthService.asBinder() 123 | } 124 | 125 | override fun getActiveMode(): Int { 126 | return XposedInit.activeMode.ordinal 127 | } 128 | 129 | override fun syncPrefs(prefs: Map): Boolean { 130 | return try { 131 | XSharedPrefs.update(prefs as Map) 132 | true 133 | } catch (e: Exception) { 134 | XPLogUtils.log(e) 135 | false 136 | } 137 | } 138 | 139 | override fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo? { 140 | val ident = Binder.clearCallingIdentity() 141 | return try { 142 | pm.getApplicationInfo(packageName, 0, userId) 143 | } finally { 144 | Binder.restoreCallingIdentity(ident) 145 | } 146 | } 147 | 148 | override fun contains(packageName: String, userId: Int): Boolean { 149 | return helper.allBannedPkgInfo.contains(PkgInfo(packageName, userId)) 150 | } 151 | 152 | fun onPkgUninstall(packageName: String, userId: Int) { 153 | val removed = mutableListOf() 154 | helper.removePkgs(listOf(PkgInfo(packageName, userId).toString()), removed) 155 | if (removed.isNotEmpty()) { 156 | XPLogUtils.log("onPkgUninstall") 157 | removed.forEach { 158 | XPLogUtils.log(it) 159 | } 160 | postStoreJob() 161 | } 162 | } 163 | 164 | private fun trimBannedPkgInfo(predicate: (PkgInfo) -> Boolean) { 165 | val allPkgInfo = helper.allBannedPkgInfo 166 | val trimmed = allPkgInfo.filter(predicate) 167 | val removed = mutableListOf() 168 | helper.removePkgs(trimmed.map { it.toString() }, removed) 169 | if (removed.isNotEmpty()) { 170 | XPLogUtils.log("trim bannedPkgInfo") 171 | removed.forEach { 172 | XPLogUtils.log(it) 173 | } 174 | postStoreJob() 175 | } 176 | } 177 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/ui/component/SearchAppBar.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.ui.component 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.activity.compose.LocalOnBackPressedDispatcherOwner 5 | import androidx.compose.animation.AnimatedVisibility 6 | import androidx.compose.animation.fadeIn 7 | import androidx.compose.animation.fadeOut 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.text.KeyboardActions 12 | import androidx.compose.foundation.text.KeyboardOptions 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 15 | import androidx.compose.material.icons.filled.Close 16 | import androidx.compose.material.icons.filled.Search 17 | import androidx.compose.material3.* 18 | import androidx.compose.runtime.* 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.focus.FocusRequester 22 | import androidx.compose.ui.focus.focusRequester 23 | import androidx.compose.ui.focus.onFocusChanged 24 | import androidx.compose.ui.platform.LocalFocusManager 25 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 26 | import androidx.compose.ui.text.input.ImeAction 27 | import androidx.compose.ui.unit.dp 28 | 29 | @OptIn(ExperimentalMaterial3Api::class) 30 | @Composable 31 | fun SearchAppBar( 32 | title: @Composable () -> Unit, 33 | searchText: String, 34 | onSearchTextChange: (String) -> Unit, 35 | onSearchStart: () -> Unit, 36 | onClearClick: () -> Unit, 37 | navigationUp: () -> Unit, 38 | onConfirm: (() -> Unit)? = null, 39 | ) { 40 | val keyboardController = LocalSoftwareKeyboardController.current 41 | val focusRequester = remember { FocusRequester() } 42 | var onSearch by remember { mutableStateOf(false) } 43 | 44 | LaunchedEffect(onSearch) { 45 | if (onSearch) { 46 | focusRequester.requestFocus() 47 | } 48 | } 49 | 50 | DisposableEffect(Unit) { 51 | onDispose { 52 | keyboardController?.hide() 53 | } 54 | } 55 | 56 | fun exitSearch() { 57 | onSearch = false 58 | keyboardController?.hide() 59 | onClearClick() 60 | } 61 | 62 | TopAppBar( 63 | title = { 64 | Box { 65 | AnimatedVisibility( 66 | modifier = Modifier.align(Alignment.CenterStart), 67 | visible = !onSearch, 68 | enter = fadeIn(), 69 | exit = fadeOut(), 70 | content = { title() } 71 | ) 72 | 73 | AnimatedVisibility( 74 | visible = onSearch, 75 | enter = fadeIn(), 76 | exit = fadeOut() 77 | ) { 78 | val focusManager = LocalFocusManager.current 79 | OutlinedTextField( 80 | modifier = Modifier 81 | .fillMaxWidth() 82 | .padding( 83 | top = 2.dp, 84 | bottom = 2.dp, 85 | end = if (navigationUp != null) 0.dp else 14.dp 86 | ) 87 | .focusRequester(focusRequester) 88 | .onFocusChanged { focusState -> 89 | if (focusState.isFocused) onSearch = true 90 | }, 91 | value = searchText, 92 | onValueChange = onSearchTextChange, 93 | trailingIcon = { 94 | IconButton( 95 | onClick = { 96 | exitSearch() 97 | }, 98 | content = { Icon(Icons.Filled.Close, null) } 99 | ) 100 | }, 101 | maxLines = 1, 102 | singleLine = true, 103 | keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), 104 | keyboardActions = KeyboardActions( 105 | onDone = { 106 | focusManager.clearFocus() 107 | keyboardController?.hide() 108 | onConfirm?.invoke() 109 | }, 110 | ) 111 | ) 112 | } 113 | } 114 | }, 115 | navigationIcon = { 116 | val onBack = { 117 | if (onSearch) { 118 | exitSearch() 119 | } else { 120 | navigationUp() 121 | } 122 | } 123 | BackHandler(onBack = onBack) 124 | IconButton( 125 | onClick = onBack, 126 | content = { Icon(Icons.AutoMirrored.Filled.ArrowBack, null) } 127 | ) 128 | }, 129 | actions = { 130 | AnimatedVisibility( 131 | visible = !onSearch 132 | ) { 133 | IconButton( 134 | onClick = { 135 | onSearch = true 136 | onSearchStart() 137 | }, 138 | content = { Icon(Icons.Filled.Search, null) } 139 | ) 140 | } 141 | } 142 | ) 143 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/ui/component/Tooltip.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.ui.component 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.shape.RoundedCornerShape 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalDensity 10 | import androidx.compose.ui.unit.* 11 | import androidx.compose.ui.window.PopupPositionProvider 12 | import kotlin.math.roundToInt 13 | 14 | @Composable 15 | private fun rememberTooltipPositionProvider(): PopupPositionProvider { 16 | val spacing = with(LocalDensity.current) { 4.dp.toPx().roundToInt() } 17 | return remember { 18 | object : PopupPositionProvider { 19 | override fun calculatePosition( 20 | anchorBounds: IntRect, 21 | windowSize: IntSize, 22 | layoutDirection: LayoutDirection, 23 | popupContentSize: IntSize 24 | ): IntOffset { 25 | val x = anchorBounds.left + ((anchorBounds.width - popupContentSize.width) / 2) 26 | val y = anchorBounds.bottom + spacing 27 | return IntOffset(x, y) 28 | } 29 | } 30 | } 31 | } 32 | 33 | @OptIn(ExperimentalMaterial3Api::class) 34 | @Composable 35 | fun TooltipBoxWrapper( 36 | tooltipText: String, 37 | modifier: Modifier = Modifier, 38 | content: @Composable () -> Unit 39 | ) { 40 | TooltipBox( 41 | positionProvider = rememberTooltipPositionProvider(), 42 | tooltip = { 43 | Surface( 44 | color = MaterialTheme.colorScheme.inverseSurface, 45 | shape = ShapeDefaults.ExtraSmall, 46 | ) { 47 | Text( 48 | text = tooltipText, 49 | style = MaterialTheme.typography.bodySmall, 50 | modifier = Modifier.padding(4.dp) 51 | ) 52 | } 53 | }, 54 | state = rememberTooltipState(), 55 | modifier = modifier 56 | ) { 57 | content() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/ui/compositionlocal/LocalDateFormat.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.ui.compositionlocal 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import java.text.DateFormat 5 | 6 | val LocalDateFormats = compositionLocalOf { 7 | error("DateFormat is not present") 8 | } 9 | 10 | class DateFormats( 11 | val dateFormat: DateFormat, 12 | val timeFormat: DateFormat 13 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/ui/theme/Colors.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val YELLOW = Color(0xFFeed502) 6 | val YELLOW_LIGHT = Color(0xFFffff52) 7 | val SECONDARY_LIGHT = Color(0xffa9817f) 8 | 9 | val YELLOW_DARK = Color(0xFFb7a400) 10 | val SECONDARY_DARK = Color(0xFF4c2b2b) -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.ui.theme 2 | 3 | import android.os.Build 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.platform.LocalContext 8 | 9 | private val DarkColorScheme = darkColorScheme( 10 | primary = YELLOW, 11 | secondary = YELLOW_DARK, 12 | tertiary = SECONDARY_DARK 13 | ) 14 | 15 | private val LightColorScheme = lightColorScheme( 16 | primary = YELLOW, 17 | secondary = YELLOW_LIGHT, 18 | tertiary = SECONDARY_LIGHT 19 | ) 20 | 21 | 22 | @Composable 23 | fun AppTheme( 24 | darkTheme: Boolean = isSystemInDarkTheme(), 25 | // Dynamic color is available on Android 12+ 26 | dynamicColor: Boolean = true, 27 | content: @Composable () -> Unit 28 | ) { 29 | val colorScheme = when { 30 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 31 | val context = LocalContext.current 32 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 33 | } 34 | darkTheme -> DarkColorScheme 35 | else -> LightColorScheme 36 | } 37 | 38 | 39 | MaterialTheme( 40 | colorScheme = colorScheme, 41 | typography = Typography, 42 | content = content 43 | ) 44 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/ui/theme/Typo.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ), 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/utils/AssetsEx.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.utils 2 | 3 | import android.content.res.AssetManager 4 | import java.io.File 5 | 6 | fun AssetManager.copyTo(path: String, dest: String) { 7 | if (list(path).isNullOrEmpty()) { 8 | copyFileTo(path, dest) 9 | } else { 10 | copyDirTo(path, dest) 11 | } 12 | } 13 | 14 | private fun AssetManager.copyDirTo(path: String, dest: String) { 15 | File(dest, path).mkdir() 16 | list(path)?.forEach { 17 | copyTo(path + File.separator + it, dest) 18 | } 19 | } 20 | 21 | private fun AssetManager.copyFileTo(filename: String, dest: String) { 22 | val input = open(filename) 23 | input.use { from -> 24 | File(dest, filename).outputStream().use { to -> 25 | from.copyTo(to) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/utils/BiometricUtils.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.utils 2 | 3 | import android.content.Context 4 | import androidx.biometric.BiometricManager 5 | import androidx.biometric.BiometricManager.Authenticators 6 | import androidx.biometric.BiometricPrompt 7 | import androidx.biometric.BiometricPrompt.AuthenticationResult 8 | import androidx.biometric.BiometricPrompt.ERROR_USER_CANCELED 9 | import androidx.compose.ui.util.fastReduce 10 | import androidx.fragment.app.FragmentActivity 11 | import kotlinx.coroutines.CancellableContinuation 12 | import kotlinx.coroutines.suspendCancellableCoroutine 13 | import kotlin.coroutines.resume 14 | 15 | object BiometricUtils { 16 | 17 | private fun createAuthCallback(cont: CancellableContinuation>) = 18 | object : BiometricPrompt.AuthenticationCallback() { 19 | 20 | private var count = 5 21 | 22 | override fun onAuthenticationSucceeded(result: AuthenticationResult) { 23 | cont.resume(Result.success(true)) 24 | } 25 | 26 | override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { 27 | if (errorCode == ERROR_USER_CANCELED && count <= 0) { 28 | cont.resume(Result.success(false)) 29 | return 30 | } 31 | cont.resume(Result.success(false)) 32 | } 33 | 34 | override fun onAuthenticationFailed() { 35 | count -= 1 36 | if (count <= 0) { 37 | biometricPrompt?.cancelAuthentication() 38 | } 39 | } 40 | } 41 | 42 | private var biometricPrompt: BiometricPrompt? = null 43 | 44 | private val auths = intArrayOf(Authenticators.BIOMETRIC_WEAK, Authenticators.DEVICE_CREDENTIAL) 45 | 46 | suspend fun auth(context: Context, title: String, description: String): Result { 47 | val bm = BiometricManager.from(context) 48 | val canAuths = auths.filter { bm.canAuthenticate(it) == BiometricManager.BIOMETRIC_SUCCESS } 49 | if (canAuths.isEmpty()) { 50 | // no auths default return true 51 | return Result.success(true) 52 | } 53 | return suspendCancellableCoroutine { 54 | val info = BiometricPrompt.PromptInfo.Builder() 55 | .setAllowedAuthenticators(canAuths.fastReduce { acc, i -> acc or i }) 56 | .setTitle(title) 57 | .setDescription(description) 58 | .build() 59 | biometricPrompt = 60 | BiometricPrompt(context as FragmentActivity, createAuthCallback(it)).also { 61 | it.authenticate(info) 62 | } 63 | } 64 | } 65 | 66 | private fun createCryptoObject(): BiometricPrompt.CryptoObject { 67 | TODO() 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/utils/Bugreport.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.utils 2 | 3 | import android.annotation.SuppressLint 4 | import cn.tinyhai.ban_uninstall.App 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import java.io.File 8 | import java.text.SimpleDateFormat 9 | import java.util.GregorianCalendar 10 | 11 | @SuppressLint("SimpleDateFormat") 12 | suspend fun getLogcatFile() = withContext(Dispatchers.IO) { 13 | val cacheDir = App.app.cacheDir 14 | val bugreportDir = File(cacheDir, "bugreport").also { it.mkdirs() } 15 | val time = GregorianCalendar.getInstance().time 16 | val formatter = SimpleDateFormat("yyyy-MM-dd_HH_mm") 17 | val logcatFile = File(bugreportDir, "logcat.txt") 18 | fastResultWithRootShell("logcat", "-d", ">", logcatFile.absolutePath) 19 | val bugreportFile = File(cacheDir, "BanUninstall_bugreport_${formatter.format(time)}.tar.gz") 20 | fastResultWithRootShell( 21 | "tar", 22 | "-czf", 23 | bugreportFile.absolutePath, 24 | "-C", 25 | bugreportDir.absolutePath, 26 | "." 27 | ) 28 | fastResultWithRootShell( 29 | "rm", 30 | "-rf", 31 | bugreportDir.absolutePath 32 | ) 33 | fastResultWithRootShell( 34 | "chmod", 35 | "0644", 36 | bugreportFile.absolutePath 37 | ) 38 | bugreportFile 39 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/utils/Cli.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.utils 2 | 3 | import android.os.Environment 4 | import android.util.Log 5 | import cn.tinyhai.ban_uninstall.App 6 | import cn.tinyhai.ban_uninstall.BuildConfig 7 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_FILE_NAME 8 | import com.topjohnwu.superuser.Shell 9 | import com.topjohnwu.superuser.Shell.FLAG_NON_ROOT_SHELL 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.withContext 12 | import java.io.File 13 | 14 | private const val TAG = "Cli" 15 | 16 | private const val TMP_PATH = "/data/local/tmp" 17 | 18 | private const val LSPATCH_PATH = "$TMP_PATH/lspatch" 19 | 20 | private val META_LOADER_LIB_PATH = "$LSPATCH_PATH/so/${getCurrentAbi()}/libmeta_loader.so" 21 | 22 | private val nativeLibraryDir = App.app.applicationInfo.nativeLibraryDir 23 | 24 | private val injectToolPath = nativeLibraryDir + File.separator + "libinject_tool.so" 25 | 26 | private val normalShell by lazy { Shell.Builder.create().setFlags(FLAG_NON_ROOT_SHELL).build() } 27 | 28 | private val rootShell by lazy { Shell.getShell() } 29 | 30 | val hasRoot get() = rootShell.isAlive && rootShell.isRoot 31 | 32 | suspend fun fastResultWithRootShell(vararg cmd: String) = withContext(Dispatchers.IO) { 33 | val out = ArrayList() 34 | val err = ArrayList() 35 | val joinedCmd = cmd.joinToString(" ") 36 | rootShell.newJob().add(joinedCmd).to(out, err).exec().isSuccess.also { 37 | Log.i(TAG, out.toString()) 38 | Log.i(TAG, err.toString()) 39 | if (!it) { 40 | Log.d(TAG, "exec $joinedCmd failed") 41 | } 42 | } 43 | } 44 | 45 | suspend fun fastResultWithShell(vararg cmd: String) = withContext(Dispatchers.IO) { 46 | val out = ArrayList() 47 | val err = ArrayList() 48 | val joinedCmd = cmd.joinToString(" ") 49 | normalShell.newJob().add(joinedCmd).to(out, err).exec().isSuccess.also { 50 | Log.i(TAG, out.toString()) 51 | Log.i(TAG, err.toString()) 52 | if (!it) { 53 | Log.d(TAG, "exec $joinedCmd failed") 54 | } 55 | } 56 | } 57 | 58 | suspend fun tryToInjectIntoSystemServer(): Boolean { 59 | return copyPatchToTmp() && copyPrefsToTmp(SP_FILE_NAME) && setFilesPermission() && injectSystemServer() 60 | } 61 | 62 | private suspend fun copyPatchToTmp() = withContext(Dispatchers.IO) { 63 | val cacheDir = App.app.cacheDir.absolutePath 64 | App.app.assets.copyTo("lspatch", App.app.cacheDir.absolutePath) 65 | val from = cacheDir + File.separator + "lspatch" 66 | val to = "/data/local/tmp" 67 | Shell.enableVerboseLogging = true 68 | fastResultWithRootShell("cp", "-R", from, to) 69 | } 70 | 71 | private suspend fun copyPrefsToTmp(filename: String) = withContext(Dispatchers.IO) { 72 | val prefName = filename.let { if (it.endsWith(".xml")) it else "$it.xml" } 73 | val appDataDir = 74 | Environment.getDataDirectory().absolutePath + File.separator + "data" + File.separator + BuildConfig.APPLICATION_ID 75 | val prefDir = appDataDir + File.separator + "shared_prefs" 76 | val prefFile = prefDir + File.separator + prefName 77 | if (File(prefFile).exists()) { 78 | fastResultWithRootShell("cp", prefFile, LSPATCH_PATH) 79 | } else { 80 | true 81 | } 82 | } 83 | 84 | private suspend fun setFilesPermission() = withContext(Dispatchers.IO) { 85 | fastResultWithRootShell( 86 | "chown", 87 | "-R", 88 | "system:system", 89 | LSPATCH_PATH 90 | ) && fastResultWithRootShell("chcon", "-R", "u:object_r:system_file:s0", LSPATCH_PATH) 91 | } 92 | 93 | private suspend fun injectSystemServer() = withContext(Dispatchers.IO) { 94 | fastResultWithRootShell( 95 | injectToolPath, 96 | "inject", 97 | "-c", 98 | "system_server", 99 | "-s", 100 | META_LOADER_LIB_PATH 101 | ) 102 | } 103 | 104 | private fun getCurrentAbi(): String { 105 | val VMRuntime = Class.forName("dalvik.system.VMRuntime") 106 | val getRuntime = VMRuntime.getDeclaredMethod("getRuntime") 107 | getRuntime.isAccessible = true 108 | val vmInstructionSet = VMRuntime.getDeclaredMethod("vmInstructionSet") 109 | vmInstructionSet.isAccessible = true 110 | val arch = vmInstructionSet.invoke(getRuntime.invoke(null)) as String 111 | return when (arch) { 112 | "arm" -> "armeabi-v7a" 113 | "arm64" -> "arm64-v8a" 114 | else -> arch 115 | } 116 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.utils 2 | 3 | import java.io.BufferedWriter 4 | import java.io.File 5 | 6 | inline fun File.writeWithBak(block: BufferedWriter.() -> Unit): Result { 7 | val bak = File("${this.path}.bak") 8 | return runCatching { 9 | if (this.exists()) { 10 | this.renameTo(bak) 11 | } 12 | bufferedWriter().use(block) 13 | }.onFailure { 14 | if (this.exists()) { 15 | this.delete() 16 | } 17 | if (bak.exists()) { 18 | bak.renameTo(this) 19 | } 20 | }.onSuccess { 21 | if (bak.exists()) { 22 | bak.delete() 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/utils/HandlerUtils.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.utils 2 | 3 | import android.os.Handler 4 | import android.os.HandlerThread 5 | import android.os.Looper 6 | 7 | object HandlerUtils { 8 | private val handler: Handler = Handler(Looper.getMainLooper()) 9 | 10 | fun checkMainThread(): Boolean { 11 | return handler.looper == Looper.myLooper() 12 | } 13 | 14 | fun checkWorkerThread(): Boolean { 15 | return workerHandler.looper == Looper.myLooper() 16 | } 17 | 18 | fun postDelay(runnable: Runnable, delayMs: Long) { 19 | handler.postDelayed(runnable, delayMs) 20 | } 21 | 22 | fun removeRunnable(runnable: Runnable) { 23 | handler.removeCallbacks(runnable) 24 | } 25 | 26 | fun postWorker(runnable: Runnable) { 27 | workerHandler.post(runnable) 28 | } 29 | 30 | fun postWorkerDelay(runnable: Runnable, delayMs: Long) { 31 | workerHandler.postDelayed(runnable, delayMs) 32 | } 33 | 34 | fun removeWorkerRunnable(runnable: Runnable) { 35 | workerHandler.removeCallbacks(runnable) 36 | } 37 | 38 | private val workerHandler by lazy { 39 | val t = HandlerThread("ban_uninstall_worker") 40 | t.start() 41 | Handler(t.looper).also { it.looper.setMessageLogging { XPLogUtils.log(it) } } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/utils/SPHost.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.utils 2 | 3 | import android.content.SharedPreferences 4 | import android.content.SharedPreferences.Editor 5 | import androidx.core.content.edit 6 | import kotlin.reflect.KProperty 7 | 8 | abstract class Preference( 9 | val key: String, 10 | val default: T, 11 | val commit: Boolean 12 | ) { 13 | abstract fun SharedPreferences.get(): T 14 | abstract fun Editor.put(value: T) 15 | } 16 | 17 | class StringPreference(key: String, default: String = "", commit: Boolean = true) : 18 | Preference(key, default, commit) { 19 | override fun SharedPreferences.get(): String { 20 | return getString(key, default)!! 21 | } 22 | 23 | override fun Editor.put(value: String) { 24 | putString(key, value) 25 | } 26 | } 27 | 28 | class IntPreference(key: String, default: Int = 0, commit: Boolean = true) : 29 | Preference(key, default, commit) { 30 | override fun SharedPreferences.get(): Int { 31 | return getInt(key, default) 32 | } 33 | 34 | override fun Editor.put(value: Int) { 35 | putInt(key, value) 36 | } 37 | } 38 | 39 | class LongPreference(key: String, default: Long = 0L, commit: Boolean = true) : 40 | Preference(key, default, commit) { 41 | override fun SharedPreferences.get(): Long { 42 | return getLong(key, default) 43 | } 44 | 45 | override fun Editor.put(value: Long) { 46 | putLong(key, value) 47 | } 48 | } 49 | 50 | class FloatPreference(key: String, default: Float = 0f, commit: Boolean = true) : 51 | Preference(key, default, commit) { 52 | override fun SharedPreferences.get(): Float { 53 | return getFloat(key, default) 54 | } 55 | 56 | override fun Editor.put(value: Float) { 57 | putFloat(key, value) 58 | } 59 | } 60 | 61 | class BooleanPreference(key: String, default: Boolean = false, commit: Boolean = true) : 62 | Preference(key, default, commit) { 63 | override fun SharedPreferences.get(): Boolean { 64 | return getBoolean(key, default) 65 | } 66 | 67 | override fun Editor.put(value: Boolean) { 68 | putBoolean(key, value) 69 | } 70 | } 71 | 72 | class StringSetPreference(key: String, default: Set = emptySet(), commit: Boolean = true) : 73 | Preference>(key, default, commit) { 74 | override fun SharedPreferences.get(): Set { 75 | return getStringSet(key, default)!! 76 | } 77 | 78 | override fun Editor.put(value: Set) { 79 | putStringSet(key, value) 80 | } 81 | } 82 | 83 | interface SPHost { 84 | companion object { 85 | const val SP_FILE_NAME = "ban_uninstall" 86 | const val SP_KEY_BAN_UNINSTALL = "sp_ban_uninstall" 87 | const val SP_KEY_BAN_CLEAR_DATA = "sp_ban_clear_data" 88 | const val SP_KEY_DEV_MODE = "sp_dev_mode" 89 | const val SP_KEY_USE_BANNED_LIST = "sp_key_use_banned_list" 90 | const val SP_KEY_SHOW_CONFIRM = "sp_key_use_show_confirm" 91 | } 92 | 93 | val prefs: SharedPreferences 94 | 95 | operator fun Preference.getValue(thisObj: Any?, property: KProperty<*>): T { 96 | return with(prefs) { get() } 97 | } 98 | 99 | operator fun Preference.setValue(thisObj: Any?, property: KProperty<*>, value: T) { 100 | prefs.edit(commit) { 101 | put(value) 102 | } 103 | } 104 | 105 | val isBanUninstall: Boolean 106 | 107 | val isBanClearData: Boolean 108 | 109 | val isDevMode: Boolean 110 | 111 | val isUseBannedList: Boolean 112 | 113 | val isShowConfirm: Boolean 114 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/utils/SharedPrefs.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import cn.tinyhai.ban_uninstall.App 6 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_FILE_NAME 7 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_KEY_BAN_CLEAR_DATA 8 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_KEY_BAN_UNINSTALL 9 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_KEY_DEV_MODE 10 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_KEY_SHOW_CONFIRM 11 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_KEY_USE_BANNED_LIST 12 | 13 | object SharedPrefs : SPHost { 14 | var isWorldReadable: Boolean = false 15 | private set 16 | 17 | override val prefs: SharedPreferences = try { 18 | App.app.getSharedPreferences(SP_FILE_NAME, Context.MODE_WORLD_READABLE).also { 19 | isWorldReadable = true 20 | } 21 | } catch (e: Exception) { 22 | App.app.getSharedPreferences(SP_FILE_NAME, Context.MODE_PRIVATE) 23 | } 24 | 25 | override var isBanUninstall by BooleanPreference(SP_KEY_BAN_UNINSTALL, true) 26 | 27 | override var isBanClearData by BooleanPreference(SP_KEY_BAN_CLEAR_DATA, true) 28 | 29 | override var isDevMode by BooleanPreference(SP_KEY_DEV_MODE, false) 30 | 31 | override var isUseBannedList by BooleanPreference(SP_KEY_USE_BANNED_LIST, false) 32 | 33 | override var isShowConfirm by BooleanPreference(SP_KEY_SHOW_CONFIRM, false) 34 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/utils/SystemContextHolder.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | 6 | @SuppressLint("StaticFieldLeak") 7 | object SystemContextHolder { 8 | private val callbacks = ArrayList() 9 | 10 | private var systemContext: Context? = null 11 | val context: Context get() = systemContext ?: throw IllegalStateException() 12 | 13 | inline fun withSystemContext(block: Context.() -> T): T { 14 | return context.block() 15 | } 16 | 17 | fun onSystemContext(context: Context) { 18 | this.systemContext = context 19 | val callbacks = callbacks.toList().also { callbacks.clear() } 20 | callbacks.forEach { 21 | it.onSystemContext(context) 22 | } 23 | } 24 | 25 | fun registerCallback(callback: Callback) { 26 | if (systemContext == null) { 27 | callbacks.add(callback) 28 | } else { 29 | callback.onSystemContext(context) 30 | } 31 | } 32 | 33 | fun interface Callback { 34 | fun onSystemContext(context: Context) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/utils/XPLogUtils.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.utils 2 | 3 | import cn.tinyhai.ban_uninstall.BuildConfig 4 | import cn.tinyhai.xp.hook.logger.XPLogger 5 | import de.robv.android.xposed.XposedBridge 6 | 7 | object XPLogUtils : XPLogger { 8 | private const val PREFIX = "[Ban Uninstall] >>> " 9 | 10 | internal var devMode = BuildConfig.DEBUG || XSharedPrefs.isDevMode 11 | 12 | fun log(log: String) { 13 | XposedBridge.log("$PREFIX$log") 14 | } 15 | 16 | fun log(th: Throwable) { 17 | XposedBridge.log("$PREFIX error >>>>>>>>>>>>>") 18 | XposedBridge.log(th) 19 | XposedBridge.log("$PREFIX error <<<<<<<<<<<<<") 20 | } 21 | 22 | override fun info(s: String) { 23 | log(s) 24 | } 25 | 26 | override fun debug(s: String) { 27 | if (devMode) { 28 | log(s) 29 | } 30 | } 31 | 32 | override fun verbose(s: String) { 33 | debug(s) 34 | } 35 | 36 | override fun error(s: String) { 37 | log(s) 38 | } 39 | 40 | override fun error(th: Throwable) { 41 | log(th) 42 | } 43 | 44 | override fun error(s: String, th: Throwable) { 45 | log(s) 46 | log(th) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/utils/XSharedPrefs.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.* 5 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener 6 | import android.os.Handler 7 | import cn.tinyhai.ban_uninstall.App 8 | import cn.tinyhai.ban_uninstall.BuildConfig 9 | import cn.tinyhai.ban_uninstall.auth.server.AuthService 10 | import cn.tinyhai.ban_uninstall.configs.Configs 11 | import cn.tinyhai.ban_uninstall.receiver.PackageChangeReceiver 12 | import cn.tinyhai.ban_uninstall.transact.server.TransactService 13 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_FILE_NAME 14 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_KEY_BAN_CLEAR_DATA 15 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_KEY_BAN_UNINSTALL 16 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_KEY_DEV_MODE 17 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_KEY_SHOW_CONFIRM 18 | import cn.tinyhai.ban_uninstall.utils.SPHost.Companion.SP_KEY_USE_BANNED_LIST 19 | import de.robv.android.xposed.XSharedPreferences 20 | 21 | private class MapPreferences( 22 | initMap: Map = emptyMap() 23 | ) : SharedPreferences { 24 | 25 | @Volatile 26 | private var map: Map = initMap 27 | 28 | fun update(map: Map) { 29 | this.map = map 30 | } 31 | 32 | override fun getAll(): Map { 33 | return HashMap(map) 34 | } 35 | 36 | override fun getString(key: String, defValue: String?): String? { 37 | return map[key] as? String ?: defValue 38 | } 39 | 40 | override fun getStringSet(key: String, defValues: Set?): Set? { 41 | return map[key] as? Set ?: defValues 42 | } 43 | 44 | override fun getInt(key: String, defValue: Int): Int { 45 | return map[key] as? Int ?: defValue 46 | } 47 | 48 | override fun getLong(key: String, defValue: Long): Long { 49 | return map[key] as? Long ?: defValue 50 | } 51 | 52 | override fun getFloat(key: String, defValue: Float): Float { 53 | return map[key] as? Float ?: defValue 54 | } 55 | 56 | override fun getBoolean(key: String, defValue: Boolean): Boolean { 57 | return map[key] as? Boolean ?: defValue 58 | } 59 | 60 | override fun contains(key: String): Boolean { 61 | return map.containsKey(key) 62 | } 63 | 64 | override fun edit(): SharedPreferences.Editor { 65 | throw UnsupportedOperationException() 66 | } 67 | 68 | override fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener?) { 69 | throw UnsupportedOperationException() 70 | } 71 | 72 | override fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener?) { 73 | throw UnsupportedOperationException() 74 | } 75 | 76 | override fun toString(): String { 77 | return map.toString() 78 | } 79 | } 80 | 81 | @SuppressLint("PrivateApi") 82 | object XSharedPrefs : SPHost { 83 | private val _prefs: MapPreferences 84 | override val prefs: SharedPreferences get() = _prefs 85 | 86 | private val listeners = ArrayList<() -> Unit>() 87 | 88 | private val notifyListenerJob by lazy { 89 | Runnable { 90 | val listeners = listeners.toList() 91 | XPLogUtils.log("notifyListener") 92 | listeners.forEach { 93 | it() 94 | } 95 | } 96 | } 97 | 98 | init { 99 | val xprefs = XSharedPreferences(BuildConfig.APPLICATION_ID, SP_FILE_NAME) 100 | _prefs = MapPreferences(xprefs.all) 101 | 102 | XPLogUtils.log(xprefs.file.absolutePath) 103 | XPLogUtils.log(prefs.toString()) 104 | } 105 | 106 | override val isBanUninstall by BooleanPreference(SP_KEY_BAN_UNINSTALL, true) 107 | 108 | override val isBanClearData by BooleanPreference(SP_KEY_BAN_CLEAR_DATA, true) 109 | 110 | override val isDevMode by BooleanPreference(SP_KEY_DEV_MODE, false) 111 | 112 | override val isUseBannedList by BooleanPreference(SP_KEY_USE_BANNED_LIST, false) 113 | 114 | override val isShowConfirm by BooleanPreference(SP_KEY_SHOW_CONFIRM, false) 115 | 116 | fun registerPrefsChangeListener(onPrefsChange: () -> Unit) { 117 | listeners.add(onPrefsChange) 118 | } 119 | 120 | fun update(map: Map) { 121 | val oldSnapshot = getPrefsSnapshot() 122 | _prefs.update(map) 123 | val newSnapshot = getPrefsSnapshot() 124 | if (!oldSnapshot.contentEquals(newSnapshot)) { 125 | postNotifyReloadListener() 126 | } 127 | } 128 | 129 | private fun getPrefsSnapshot(): BooleanArray { 130 | return booleanArrayOf(isBanUninstall, isBanClearData) 131 | } 132 | 133 | private fun postNotifyReloadListener() { 134 | XPLogUtils.log("postNotifyReloadListener") 135 | HandlerUtils.postDelay(notifyListenerJob, 0) 136 | } 137 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/vm/AuthViewModel.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.vm 2 | 3 | import androidx.lifecycle.ViewModel 4 | import cn.tinyhai.ban_uninstall.auth.client.AuthClient 5 | import cn.tinyhai.ban_uninstall.auth.entities.AuthData 6 | 7 | class AuthViewModel : ViewModel() { 8 | 9 | companion object { 10 | private const val TAG = "AuthViewModel" 11 | } 12 | 13 | private var handled: Boolean = false 14 | 15 | private lateinit var authClient: AuthClient 16 | 17 | lateinit var authData: AuthData 18 | private set 19 | 20 | val hasPwd get() = authClient.hasPwd() 21 | 22 | fun setup(authClient: AuthClient, authData: AuthData) { 23 | this.authClient = authClient 24 | this.authData = authData 25 | } 26 | 27 | fun onAgree() { 28 | if (handled) { 29 | return 30 | } 31 | authClient.agree(authData.opId) 32 | handled = true 33 | } 34 | 35 | fun onPrevent() { 36 | if (handled) { 37 | return 38 | } 39 | authClient.prevent(authData.opId) 40 | handled = true 41 | } 42 | 43 | fun isValid(): Boolean { 44 | return authClient.isValid(authData.opId) 45 | } 46 | 47 | fun authenticate(pwd: String): Boolean { 48 | return authClient.authenticate(pwd) 49 | } 50 | 51 | override fun onCleared() { 52 | super.onCleared() 53 | if (!handled) { 54 | onPrevent() 55 | handled = true 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/vm/BannedAppViewModel.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.vm 2 | 3 | import android.graphics.drawable.Drawable 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import cn.tinyhai.ban_uninstall.App 7 | import cn.tinyhai.ban_uninstall.transact.client.TransactClient 8 | import cn.tinyhai.ban_uninstall.transact.entities.PkgInfo 9 | import cn.tinyhai.ban_uninstall.utils.HanziToPinyin 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.asStateFlow 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.withContext 15 | 16 | data class AppInfo( 17 | val label: String, 18 | val icon: Drawable, 19 | val pkgInfo: PkgInfo 20 | ) { 21 | val isDual get() = pkgInfo.userId > 1 22 | val key get() = pkgInfo.toString() 23 | } 24 | 25 | data class BannedAppState( 26 | val isRefreshing: Boolean = false, 27 | val originFreedAppInfos: List = emptyList(), 28 | val originBannedAppInfos: List = emptyList(), 29 | val selectedInFreed: List = emptyList(), 30 | val selectedInBanned: List = emptyList(), 31 | val query: String = "", 32 | ) { 33 | 34 | val freedAppInfos: List 35 | get() { 36 | return if (query.isEmpty()) { 37 | originFreedAppInfos 38 | } else { 39 | val queryPinyin = HanziToPinyin.getInstance().toPinyin(query).lowercase() 40 | val queryLower = query.lowercase() 41 | originFreedAppInfos.filter { 42 | val labelPinyin = HanziToPinyin.getInstance().toPinyin(it.label).lowercase() 43 | labelPinyin.contains(queryPinyin) 44 | || it.pkgInfo.packageName.lowercase().contains(queryLower) 45 | } 46 | }.sortedWith(comparator) 47 | } 48 | val bannedAppInfos: List 49 | get() { 50 | return if (query.isEmpty()) { 51 | originBannedAppInfos 52 | } else { 53 | val queryPinyin = HanziToPinyin.getInstance().toPinyin(query).lowercase() 54 | val queryLower = query.lowercase() 55 | originBannedAppInfos.filter { 56 | val labelPinyin = HanziToPinyin.getInstance().toPinyin(it.label).lowercase() 57 | labelPinyin.contains(queryPinyin) 58 | || it.pkgInfo.packageName.lowercase().contains(queryLower) 59 | } 60 | }.sortedWith(comparator) 61 | } 62 | 63 | companion object { 64 | val Empty = BannedAppState() 65 | 66 | private val comparator: Comparator by lazy { 67 | compareBy { 68 | HanziToPinyin.getInstance().toPinyin(it.label) 69 | } then compareBy { 70 | it.pkgInfo.packageName 71 | } then compareBy { 72 | it.isDual 73 | } 74 | } 75 | } 76 | } 77 | 78 | class BannedAppViewModel : ViewModel() { 79 | 80 | companion object { 81 | private const val TAG = "BannedAppViewModel" 82 | } 83 | 84 | private val tempOutput = ArrayList() 85 | get() { 86 | field.clear() 87 | return field 88 | } 89 | 90 | private val client = TransactClient() 91 | 92 | private val _state = MutableStateFlow(BannedAppState.Empty) 93 | 94 | val state = _state.asStateFlow() 95 | 96 | fun refresh() { 97 | viewModelScope.launch { 98 | updateState(_state) { it.copy(isRefreshing = true) } 99 | val pm = App.app.packageManager 100 | withContext(Dispatchers.IO) { 101 | val allPackages = client.packages.list 102 | val appInfoList = allPackages.map { 103 | val label = it.applicationInfo.loadLabel(pm).toString() 104 | val icon = it.applicationInfo.loadIcon(pm) 105 | val packageName = it.packageName ?: it.applicationInfo.packageName 106 | val uid = it.applicationInfo.uid 107 | AppInfo(label, icon, PkgInfo(packageName, uid / 100_000)) 108 | } 109 | val bannedPkgInfos = client.allBannedPackages.map { PkgInfo(it) } 110 | val bannedAppInfos = ArrayList() 111 | val freedPkgInfos = appInfoList.toMutableList().apply { 112 | for (bannedPkgInfo in bannedPkgInfos) { 113 | val idx = indexOfFirst { it.pkgInfo == bannedPkgInfo } 114 | if (idx >= 0) { 115 | bannedAppInfos.add(removeAt(idx)) 116 | } 117 | } 118 | } 119 | updateState(_state) { 120 | it.copy( 121 | isRefreshing = false, 122 | originFreedAppInfos = freedPkgInfos, 123 | originBannedAppInfos = bannedAppInfos, 124 | selectedInFreed = emptyList(), 125 | selectedInBanned = emptyList(), 126 | query = "" 127 | ) 128 | } 129 | } 130 | } 131 | } 132 | 133 | fun onBanPkgs(pkgInfos: List) { 134 | viewModelScope.launch { 135 | val banned = tempOutput 136 | withContext(Dispatchers.IO) { 137 | client.banPackage(pkgInfos.map { it.toString() }, banned) 138 | } 139 | 140 | if (banned.isEmpty()) { 141 | return@launch 142 | } 143 | 144 | val bannedPkgInfos = banned.map { PkgInfo(it) } 145 | updateState(_state) { 146 | val newFreePkgInfos = it.originFreedAppInfos.toMutableList() 147 | val newBannedPkgInfos = it.originBannedAppInfos.toMutableList() 148 | newFreePkgInfos.moveTo(newBannedPkgInfos) { 149 | it.pkgInfo in bannedPkgInfos 150 | } 151 | it.copy( 152 | originFreedAppInfos = newFreePkgInfos, 153 | originBannedAppInfos = newBannedPkgInfos 154 | ) 155 | } 156 | } 157 | } 158 | 159 | fun onFreePkgs(pkgInfos: List) { 160 | viewModelScope.launch { 161 | val freed = tempOutput 162 | withContext(Dispatchers.IO) { 163 | client.freePackage(pkgInfos.map { it.toString() }, freed) 164 | } 165 | if (freed.isEmpty()) { 166 | return@launch 167 | } 168 | 169 | val freedPkgInfos = freed.map { PkgInfo(it) } 170 | updateState(_state) { 171 | val newFreePkgInfos = it.originFreedAppInfos.toMutableList() 172 | val newBannedPkgInfos = it.originBannedAppInfos.toMutableList() 173 | newBannedPkgInfos.moveTo(newFreePkgInfos) { 174 | it.pkgInfo in freedPkgInfos 175 | } 176 | it.copy( 177 | originFreedAppInfos = newFreePkgInfos, 178 | originBannedAppInfos = newBannedPkgInfos 179 | ) 180 | } 181 | } 182 | } 183 | 184 | fun onFreedAppClick(appInfo: AppInfo) { 185 | updateState(_state) { 186 | val newList = it.selectedInFreed.toMutableList().apply { 187 | val idx = indexOf(appInfo) 188 | if (idx < 0) { 189 | add(appInfo) 190 | } else { 191 | removeAt(idx) 192 | } 193 | } 194 | it.copy(selectedInFreed = newList) 195 | } 196 | } 197 | 198 | fun onFreedSelectAll() { 199 | updateState(_state) { 200 | it.copy(selectedInFreed = it.freedAppInfos) 201 | } 202 | } 203 | 204 | fun onFreeSelectedBanned() { 205 | val selectedInBanned = state.value.selectedInBanned.map { it.pkgInfo } 206 | onFreePkgs(selectedInBanned) 207 | clearSelected() 208 | } 209 | 210 | fun onBanSelectedFreed() { 211 | val selectedInFreed = state.value.selectedInFreed.map { it.pkgInfo } 212 | onBanPkgs(selectedInFreed) 213 | clearSelected() 214 | } 215 | 216 | fun onBannedSelectAll() { 217 | updateState(_state) { 218 | it.copy(selectedInBanned = it.bannedAppInfos) 219 | } 220 | } 221 | 222 | fun clearSelected() { 223 | updateState(_state) { 224 | it.copy(selectedInFreed = emptyList(), selectedInBanned = emptyList()) 225 | } 226 | } 227 | 228 | fun onBannedAppClick(appInfo: AppInfo) { 229 | updateState(_state) { 230 | val newList = it.selectedInBanned.toMutableList().apply { 231 | val idx = indexOf(appInfo) 232 | if (idx < 0) { 233 | add(appInfo) 234 | } else { 235 | removeAt(idx) 236 | } 237 | } 238 | it.copy(selectedInBanned = newList) 239 | } 240 | } 241 | 242 | fun onQueryChange(newQuery: String) { 243 | updateState(_state) { it.copy(query = newQuery.trim()) } 244 | } 245 | 246 | fun onSearchClear() { 247 | updateState(_state) { it.copy(query = "") } 248 | } 249 | 250 | private fun MutableList.moveTo( 251 | desList: MutableList, 252 | predicate: (AppInfo) -> Boolean 253 | ) { 254 | val itor = iterator() 255 | while (itor.hasNext()) { 256 | val appInfo = itor.next() 257 | if (predicate(appInfo)) { 258 | itor.remove() 259 | desList.add(appInfo) 260 | } 261 | } 262 | } 263 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/vm/OpRecordViewModel.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.vm 2 | 3 | import android.graphics.drawable.Drawable 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import cn.tinyhai.ban_uninstall.App 7 | import cn.tinyhai.ban_uninstall.auth.IAuth 8 | import cn.tinyhai.ban_uninstall.auth.client.AuthClient 9 | import cn.tinyhai.ban_uninstall.auth.entities.OpResult 10 | import cn.tinyhai.ban_uninstall.auth.entities.OpType 11 | import cn.tinyhai.ban_uninstall.transact.client.TransactClient 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.asStateFlow 15 | import kotlinx.coroutines.launch 16 | import kotlinx.coroutines.withContext 17 | import java.util.Date 18 | 19 | data class OpRecordInfo( 20 | val label: String?, 21 | val isDual: Boolean, 22 | val packageName: String, 23 | val icon: Drawable?, 24 | val opType: OpType, 25 | val opResult: OpResult, 26 | val opUid: Int, 27 | val opLabel: String?, 28 | val opDate: Date, 29 | ) 30 | 31 | 32 | data class OpRecordState( 33 | val isRefreshing: Boolean = false, 34 | val hasPwd: Boolean = false, 35 | val showAllowed: Boolean = true, 36 | val showPrevented: Boolean = true, 37 | val showUninstall: Boolean = true, 38 | val showClearData: Boolean = true, 39 | val records: List = emptyList() 40 | ) { 41 | companion object { 42 | val Empty = OpRecordState() 43 | } 44 | } 45 | 46 | class OpRecordViewModel : ViewModel() { 47 | private val transactClient: TransactClient = TransactClient() 48 | 49 | private val authClient = 50 | transactClient.auth?.let { AuthClient(IAuth.Stub.asInterface(it)) } ?: AuthClient() 51 | 52 | private val _state = MutableStateFlow(OpRecordState.Empty) 53 | 54 | val state = _state.asStateFlow() 55 | 56 | init { 57 | viewModelScope.launch { 58 | val hasPwd = authClient.hasPwd() 59 | updateState(_state) { 60 | it.copy(hasPwd = hasPwd) 61 | } 62 | } 63 | } 64 | 65 | fun refresh() { 66 | viewModelScope.launch { 67 | updateState(_state) { it.copy(isRefreshing = true) } 68 | val pm = App.app.packageManager 69 | val records = withContext(Dispatchers.IO) { 70 | authClient.allOpRecord.asReversed().map { 71 | val pkgInfo = it.pkgInfo 72 | val appInfo = 73 | transactClient.getApplicationInfoAsUser(pkgInfo.packageName, pkgInfo.userId) 74 | val opAppInfo = transactClient.getApplicationInfoAsUser( 75 | it.callingPackageName, 76 | it.callingUid / 100_000 77 | ) 78 | OpRecordInfo( 79 | label = it.label, 80 | isDual = pkgInfo.userId / 100_000 > 0, 81 | packageName = appInfo?.packageName ?: pkgInfo.packageName, 82 | icon = appInfo?.loadIcon(pm), 83 | opType = it.opType, 84 | opResult = it.result, 85 | opUid = it.callingUid, 86 | opLabel = opAppInfo?.loadLabel(pm)?.toString(), 87 | opDate = Date(it.timeMillis) 88 | ) 89 | } 90 | } 91 | updateState(_state) { 92 | it.copy(records = records, isRefreshing = false) 93 | } 94 | } 95 | } 96 | 97 | fun showAllowed(show: Boolean) { 98 | updateState(_state) { 99 | it.copy(showAllowed = show) 100 | } 101 | } 102 | 103 | fun showPrevented(show: Boolean) { 104 | updateState(_state) { 105 | it.copy(showPrevented = show) 106 | } 107 | } 108 | 109 | fun showUninstall(show: Boolean) { 110 | updateState(_state) { 111 | it.copy(showUninstall = show) 112 | } 113 | } 114 | 115 | fun showClearData(show: Boolean) { 116 | updateState(_state) { 117 | it.copy(showClearData = show) 118 | } 119 | } 120 | 121 | fun clearAllRecords() { 122 | viewModelScope.launch { 123 | withContext(Dispatchers.IO) { 124 | authClient.clearAllOpRecord() 125 | } 126 | refresh() 127 | } 128 | } 129 | 130 | fun onVerifyPwd(pwd: String): Boolean { 131 | return authClient.authenticate(pwd) 132 | } 133 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/tinyhai/ban_uninstall/vm/VMEx.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.ban_uninstall.vm 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.launch 7 | 8 | inline fun > ViewModel.updateState( 9 | state: S, 10 | crossinline update: (T) -> T 11 | ) { 12 | viewModelScope.launch { 13 | state.value = update(state.value) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-en/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | BanUninstall 4 | Configurations 5 | Prevent app be uninstalled or app data be cleared 6 | @string/app_name 7 | Banned apps 8 | Freed apps 9 | Module status 10 | Function configs 11 | Ban uninstall apps 12 | Ban clear apps\' data 13 | Dev mode 14 | Working 15 | Not Working 16 | Running on: %1$s 17 | Sync configs to system 18 | test msg 19 | Config banned apps 20 | (Dual)%1$s 21 | Nothing here… 22 | Drag to here to add it into banned list 23 | Release to finish adding 24 | Ban on demand 25 | Advanced 26 | All 27 | Cancel 28 | Drag to here to remove it from banned list 29 | Release to finish removing 30 | Prevent 31 | Verify&Allow 32 | Clear App\'s Data 33 | Uninstall App 34 | tries to %1$s 35 | Unkown App 36 | Verify Password 37 | incorrect password 38 | Security 39 | Show confirm dialog 40 | Show confirm dialog when someone tries to uninstall or clear data 41 | Pls enter 4 to 16 pin 42 | Set pin 43 | Change pin 44 | Using a separate pin 45 | Shell 46 | System 47 | Root 48 | For confirm dialog and changing configs 49 | Pls complete the verification to allow %1$s 50 | Verification required 51 | clear data of %1$s 52 | uninstall %1$s 53 | Auto start 54 | Auto activate with root after system boot completed 55 | Activate With Root 56 | Do you want to activate it with root right now? 57 | Bug Report 58 | Uninstalled App 59 | Prevented 60 | Operation Records 61 | View operation records 62 | Allowed 63 | Unhandled 64 | Clear All Records 65 | Filter List 66 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bloqueio de desinstalação 4 | Configurações 5 | Desative a desinstalação ou a limpeza de dados dos aplicativos 6 | @string/app_name 7 | Aplicativos proibidos 8 | Aplicativos desprotegidos 9 | Estado de ativação do módulo 10 | Configuração das funções 11 | Avançado 12 | Segurança 13 | Desativar a desinstalação de aplicativos 14 | Desativar limpeza de dados 15 | Modo de desenvolvedor 16 | Proibição seletiva 17 | Mostrar pop-up de verificação 18 | A janela pop-up de verificação aparecerá ao desinstalar ou limpar os dados 19 | Usado para verificar pop-ups e modificar itens de configurações 20 | Usar senhas separadas 21 | Configurar aplicativos proibidos 22 | Alterar senha 23 | Funcionando 24 | Não está funcionando 25 | Modo operacional: %1$s 26 | Sincronizar as configurações com o sistema 27 | Mensagem de teste 28 | (Duplo)%1$s 29 | Vazio 30 | "Arraste aqui para adicionar à lista de aplicativos protegidos" 31 | Solte para concluir a adição 32 | Arraste aqui para remover da lista de aplicativos protegidos 33 | Solte para concluir a remoção 34 | Selecionar tudo 35 | Cancelar 36 | Bloquear 37 | Verificar e permitir 38 | Limpar dados do aplicativo 39 | Desinstalar aplicativo 40 | Limpar %1$s os dados 41 | Desinstalar %1$s 42 | Experimente %1$s 43 | Senha inválida 44 | Inserir uma senha de 4 a 16 dígitos 45 | Aplicativo desconhecido 46 | Shell 47 | Sistema 48 | Root 49 | Confirme sua senha 50 | Configure uma senha 51 | Verificação necessária 52 | Conclua a verificação para permitir %1$s 53 | Executar na inicialização 54 | Ativar automaticamente através do Root após a inicialização do sistema 55 | Ativar usando Root 56 | Deseja ativar com privilégios Root agora? 57 | Relatório de erro 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/scope.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | android 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 禁止卸载 3 | 配置 4 | 禁止卸载应用和清除应用数据 5 | @string/app_name 6 | 禁止的应用 7 | 未禁止的应用 8 | 模块状态 9 | 功能配置 10 | 高级 11 | 安全 12 | 禁止卸载应用 13 | 禁止清除数据 14 | 开发模式 15 | 按需禁止 16 | 显示验证弹窗 17 | 在卸载或清除数据时弹出验证弹窗 18 | 用于验证弹窗和修改配置项 19 | 使用独立密码 20 | 配置禁止应用 21 | 修改密码 22 | 工作中 23 | 未工作 24 | 运行模式: %1$s 25 | 同步配置给系统 26 | 测试消息 27 | (双开)%1$s 28 | 空空如也… 29 | "拖动到此处添加到禁止列表" 30 | 松手完成添加 31 | 拖动到此处移出禁止列表 32 | 松手完成移出 33 | 全选 34 | 取消 35 | 阻止 36 | 验证并允许 37 | 清除应用数据 38 | 卸载应用 39 | 清除 %1$s 的数据 40 | 卸载 %1$s 41 | 尝试%1$s 42 | 密码错误 43 | 请输入4–16位数字密码 44 | 未知应用 45 | Shell 46 | 系统 47 | Root 48 | 验证密码 49 | 设置密码 50 | 需要验证 51 | 请完成验证以允许%1$s 52 | 自启动 53 | 在系统启动后自动通过Root激活 54 | 使用Root激活 55 | 是否立即使用Root权限激活? 56 | 抓取日志 57 | 已卸载的应用 58 | 已阻止 59 | 操作记录 60 | 查看操作记录 61 | 已允许 62 | 未处理 63 | 清除所有记录 64 | 过滤列表 65 | -------------------------------------------------------------------------------- /app/src/main/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.support.uppercaseFirstChar 2 | 3 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 4 | plugins { 5 | alias(libs.plugins.androidApplication) apply false 6 | alias(libs.plugins.jetbrainsKotlinAndroid) apply false 7 | alias(libs.plugins.androidLibrary) apply false 8 | alias(libs.plugins.composeCompiler) apply false 9 | alias(libs.plugins.ksp) apply false 10 | alias(libs.plugins.jetbrainsKotlinJvm) apply false 11 | alias(libs.plugins.kotlinParcelize) apply false 12 | } 13 | 14 | ext["allArch"] = arrayOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64", "universal") 15 | 16 | val allArch: Array by ext 17 | 18 | tasks.register("buildRelease") { 19 | val allBuildType = allArch + "release" 20 | allBuildType.forEach { buildType -> 21 | dependsOn(":app:assemble${buildType.uppercaseFirstChar()}") 22 | } 23 | } 24 | 25 | tasks.register("buildDebug") { 26 | dependsOn(":app:assembleDebug") 27 | } -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /app/src/main/res/values/strings.xml 3 | translation: /%original_path%-%android_code%/%original_file_name% 4 | translate_attributes: 0 5 | content_segmentation: 0 6 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.4.0" 3 | androidx-appcompat = "1.6.1" 4 | biometric = "1.2.0-alpha05" 5 | libsu = "5.2.2" 6 | kotlin = "2.0.0" 7 | kotlinpoet = "1.16.0" 8 | ksp = "2.0.0-1.0.21" 9 | xposed-api = "82" 10 | compose-bom = "2024.05.00" 11 | activity-compose = "1.9.0" 12 | compose-settings = "2.4.0" 13 | parcelablelist = "2.0.1" 14 | compose-destination = "2.0.0-beta01" 15 | coil = "2.6.0" 16 | jetbrains-kotlin-jvm = "1.9.10" 17 | 18 | [libraries] 19 | androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } 20 | appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } 21 | libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } 22 | xposed-api = { group = "de.robv.android.xposed", name = "api", version.ref = "xposed-api" } 23 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } 24 | androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } 25 | androidx-compose-material = { group = "androidx.compose.material", name = "material" } 26 | androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } 27 | androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 28 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } 29 | composeSettings-ui = { group = "com.github.alorma.compose-settings", name = "ui-tiles", version.ref = "compose-settings" } 30 | composeSettings-ui-extended = { group = "com.github.alorma.compose-settings", name = "ui-tiles-extended", version.ref = "compose-settings" } 31 | androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } 32 | dev-rikka-rikkax-parcelablelist = { group = "dev.rikka.rikkax.parcelablelist", name = "parcelablelist", version.ref = "parcelablelist" } 33 | compose-destinations-core = { group = "io.github.raamcosta.compose-destinations", name = "core", version.ref = "compose-destination" } 34 | compose-destinations-bottomsheet = { group = "io.github.raamcosta.compose-destinations", name = "bottom-sheet", version.ref = "compose-destination" } 35 | compose-destinations-ksp = { group = "io.github.raamcosta.compose-destinations", name = "ksp", version.ref = "compose-destination" } 36 | coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } 37 | 38 | kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } 39 | kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" } 40 | ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } 41 | 42 | [plugins] 43 | androidApplication = { id = "com.android.application", version.ref = "agp" } 44 | jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 45 | composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 46 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 47 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 48 | jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrains-kotlin-jvm" } 49 | kotlinParcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } 50 | 51 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Mar 24 15:52:18 CST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /hiddenApi/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /hiddenApi/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidLibrary) 3 | } 4 | 5 | android { 6 | namespace = "cn.tinyhai.hiddenapi" 7 | compileSdk = 34 8 | 9 | lint { 10 | abortOnError = false 11 | } 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_1_8 15 | targetCompatibility = JavaVersion.VERSION_1_8 16 | } 17 | } -------------------------------------------------------------------------------- /hiddenApi/src/main/java/android/app/ActivityThread.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | public class ActivityThread { 4 | public static ActivityThread systemMain() { 5 | throw new UnsupportedOperationException(); 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /hiddenApi/src/main/java/android/app/IActivityManager.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.os.IInterface; 4 | 5 | public interface IActivityManager extends IInterface { 6 | 7 | abstract class Stub extends android.os.Binder implements IActivityManager { 8 | public static IActivityManager asInterface(android.os.IBinder obj) { 9 | throw new UnsupportedOperationException(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /hiddenApi/src/main/java/android/content/pm/BaseParceledListSlice.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Parcelable; 4 | 5 | import java.util.List; 6 | 7 | abstract class BaseParceledListSlice implements Parcelable { 8 | public List getList() { 9 | throw new UnsupportedOperationException(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /hiddenApi/src/main/java/android/content/pm/IPackageDataObserver.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | public interface IPackageDataObserver { 4 | void onRemoveCompleted(String packageName, boolean succeeded); 5 | } 6 | -------------------------------------------------------------------------------- /hiddenApi/src/main/java/android/content/pm/IPackageDeleteObserver2.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | public interface IPackageDeleteObserver2 { 4 | void onPackageDeleted(String packageName, int returnCode, String msg); 5 | } 6 | -------------------------------------------------------------------------------- /hiddenApi/src/main/java/android/content/pm/IPackageManager.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | public interface IPackageManager extends android.os.IInterface { 4 | 5 | ParceledListSlice getInstalledPackages(long flags, int userId); 6 | 7 | PackageInfo getPackageInfo(String packageName, long flags, int userId); 8 | 9 | ApplicationInfo getApplicationInfo(String packageName, long flags, int userId); 10 | 11 | abstract class Stub extends android.os.Binder implements android.content.pm.IPackageManager { 12 | public static android.content.pm.IPackageManager asInterface(android.os.IBinder obj) { 13 | throw new UnsupportedOperationException(); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /hiddenApi/src/main/java/android/content/pm/ParceledListSlice.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class ParceledListSlice extends BaseParceledListSlice { 7 | 8 | @Override 9 | public int describeContents() { 10 | throw new UnsupportedOperationException(); 11 | } 12 | 13 | @Override 14 | public void writeToParcel(Parcel dest, int flags) { 15 | throw new UnsupportedOperationException(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /hiddenApi/src/main/java/android/ddm/DdmHandleAppName.java: -------------------------------------------------------------------------------- 1 | package android.ddm; 2 | 3 | public class DdmHandleAppName { 4 | public static void setAppName(String appName, int userId) { 5 | throw new UnsupportedOperationException(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /hiddenApi/src/main/java/android/os/IUserManager.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public interface IUserManager extends IInterface { 4 | 5 | int[] getProfileIds(int userId, boolean enabledOnly); 6 | 7 | abstract class Stub extends android.os.Binder implements IUserManager { 8 | public static IUserManager asInterface(android.os.IBinder obj) { 9 | throw new UnsupportedOperationException(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /hiddenApi/src/main/java/android/os/ServiceManager.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public class ServiceManager { 4 | public static IBinder getService(String name) { 5 | throw new UnsupportedOperationException(); 6 | } 7 | public static void addService(String name, IBinder service, boolean allowIsolated) { 8 | throw new UnsupportedOperationException(); 9 | } 10 | 11 | public static void addService(String name, IBinder service) { 12 | throw new UnsupportedOperationException(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /hook/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /hook/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.jetbrainsKotlinJvm) 3 | } 4 | 5 | dependencies { 6 | compileOnly(libs.xposed.api) 7 | } -------------------------------------------------------------------------------- /hook/src/main/java/cn/tinyhai/xp/annotation/HookScope.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.annotation 2 | 3 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class HookScope( 6 | val targetPackageName: String, 7 | val targetProcessName: String = "", 8 | val scopeName: String = "", 9 | val targetClassName: String = "", 10 | val autoRegister: Boolean = true, 11 | val isUnhookable: Boolean = false 12 | ) -------------------------------------------------------------------------------- /hook/src/main/java/cn/tinyhai/xp/annotation/HookerGate.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.annotation 2 | 3 | @Target(AnnotationTarget.FUNCTION) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class HookerGate 6 | -------------------------------------------------------------------------------- /hook/src/main/java/cn/tinyhai/xp/annotation/HookerId.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.annotation 2 | 3 | @Target(AnnotationTarget.FUNCTION) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class HookerId(val id: String) 6 | -------------------------------------------------------------------------------- /hook/src/main/java/cn/tinyhai/xp/annotation/Initiate.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.annotation 2 | 3 | @Target(AnnotationTarget.FUNCTION) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class Initiate 6 | -------------------------------------------------------------------------------- /hook/src/main/java/cn/tinyhai/xp/annotation/InjectHooker.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.annotation 2 | 3 | @Target(AnnotationTarget.FIELD) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class InjectHooker 6 | -------------------------------------------------------------------------------- /hook/src/main/java/cn/tinyhai/xp/annotation/MethodHooker.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.annotation 2 | 3 | @Target(AnnotationTarget.FUNCTION) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class MethodHooker( 6 | val methodName: String, 7 | val hookType: HookType = HookType.BeforeMethod, 8 | val className: String = "", 9 | val minSdkInclusive: Int = 0, 10 | val maxSdkExclusive: Int = Int.MAX_VALUE 11 | ) 12 | 13 | enum class HookType { 14 | BeforeMethod, 15 | ReplaceMethod, 16 | AfterMethod 17 | } 18 | -------------------------------------------------------------------------------- /hook/src/main/java/cn/tinyhai/xp/annotation/Oneshot.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.annotation 2 | 3 | @Target(AnnotationTarget.FUNCTION) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class Oneshot( 6 | val unhookable: Boolean = false 7 | ) 8 | -------------------------------------------------------------------------------- /hook/src/main/java/cn/tinyhai/xp/hook/Hooker.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.hook 2 | 3 | import cn.tinyhai.xp.hook.logger.XPLogger 4 | import de.robv.android.xposed.XC_MethodHook 5 | import de.robv.android.xposed.XC_MethodHook.Unhook 6 | import de.robv.android.xposed.XposedBridge 7 | import de.robv.android.xposed.XposedHelpers 8 | import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam 9 | 10 | interface Hooker { 11 | val name: String 12 | val targetPackageName: String 13 | val targetProcessName: String 14 | 15 | fun isInterest(lp: LoadPackageParam): Boolean { 16 | return lp.packageName == targetPackageName && (targetProcessName.isBlank() || lp.processName == targetProcessName) 17 | } 18 | 19 | fun init(lp: LoadPackageParam) 20 | fun startHook(lp: LoadPackageParam) 21 | } 22 | 23 | interface UnhookableHooker : Hooker { 24 | fun unhook() 25 | } 26 | 27 | interface OneshotHooker : Hooker { 28 | fun startOneshotHook(lp: LoadPackageParam) 29 | } 30 | 31 | interface HookerHelper { 32 | val logger: XPLogger 33 | 34 | fun findAndHookAll(clazz: Class<*>, methodName: String, callback: XC_MethodHook): List { 35 | return XposedBridge.hookAllMethods(clazz, methodName, callback).toList() 36 | } 37 | 38 | fun findAndHookAll( 39 | className: String, 40 | classLoader: ClassLoader, 41 | methodName: String, 42 | callback: XC_MethodHook 43 | ): List { 44 | return runCatching { 45 | findAndHookAll(classLoader.loadClass(className), methodName, callback) 46 | }.onFailure { 47 | logger.error(it) 48 | }.getOrElse { emptyList() } 49 | } 50 | 51 | fun findAndHookExact( 52 | clazz: Class<*>, 53 | methodName: String, 54 | vararg parametersAndCallback: Any 55 | ): Unhook? { 56 | return runCatching { 57 | XposedHelpers.findAndHookMethod(clazz, methodName, parametersAndCallback) 58 | }.onFailure { 59 | logger.error(it) 60 | }.getOrNull() 61 | } 62 | 63 | fun findAndHookExact( 64 | className: String, 65 | classLoader: ClassLoader, 66 | methodName: String, 67 | parametersAndCallback: Any 68 | ): Unhook? { 69 | return runCatching { 70 | findAndHookExact(classLoader.loadClass(className), methodName, parametersAndCallback) 71 | }.onFailure { 72 | logger.error(it) 73 | }.getOrNull() 74 | } 75 | 76 | fun findAndHookFirst( 77 | clazz: Class<*>, methodName: String, callback: XC_MethodHook 78 | ): Unhook? { 79 | val hooker = clazz.declaredMethods.firstOrNull { it.name == methodName }?.let { 80 | XposedBridge.hookMethod(it, callback).also { 81 | logger.verbose("hook ${clazz.canonicalName}#$methodName success") 82 | } 83 | } 84 | if (hooker == null) { 85 | logger.error("hook ${clazz.canonicalName}#$methodName failed !!!!!") 86 | } 87 | return hooker 88 | } 89 | 90 | fun findAndHookFirst( 91 | className: String, classLoader: ClassLoader, methodName: String, callback: XC_MethodHook 92 | ): Unhook? { 93 | return runCatching { 94 | findAndHookFirst(classLoader.loadClass(className), methodName, callback) 95 | }.onFailure { 96 | logger.error(it) 97 | }.getOrNull() 98 | } 99 | 100 | fun MutableList.hookAndAddFirst( 101 | clazz: Class<*>, 102 | methodName: String, 103 | callback: XC_MethodHook 104 | ) { 105 | findAndHookFirst(clazz, methodName, callback)?.also { add(it) } 106 | } 107 | 108 | fun MutableList.hookAndAddFirst( 109 | className: String, 110 | classLoader: ClassLoader, 111 | methodName: String, 112 | callback: XC_MethodHook 113 | ) { 114 | runCatching { 115 | findAndHookFirst(classLoader.loadClass(className), methodName, callback)?.also { add(it) } 116 | }.onFailure { 117 | logger.error(it) 118 | } 119 | } 120 | 121 | fun Hooker.runCatchingWithLog(block: () -> Unit) { 122 | logger.verbose("$name >>>>>>>>>>>>>>>>") 123 | runCatching(block).onFailure { 124 | logger.error("$name error", it) 125 | } 126 | logger.verbose("$name <<<<<<<<<<<<<<<<") 127 | } 128 | } 129 | 130 | abstract class BaseOneshotHooker( 131 | override val logger: XPLogger = XPLogger 132 | ) : OneshotHooker, HookerHelper { 133 | 134 | private var hooked = false 135 | 136 | override fun init(lp: LoadPackageParam) {} 137 | 138 | override fun startHook(lp: LoadPackageParam) { 139 | startOneshotHook(lp) 140 | } 141 | 142 | override fun startOneshotHook(lp: LoadPackageParam) { 143 | if (hooked || !isInterest(lp)) { 144 | return 145 | } 146 | hooked = true 147 | runCatchingWithLog { 148 | logger.verbose("startOneshotHook >>>>") 149 | createOneshotHook(lp) 150 | logger.verbose("startOneshotHook <<<<") 151 | } 152 | } 153 | 154 | abstract fun createOneshotHook(lp: LoadPackageParam) 155 | } 156 | 157 | abstract class BaseUnhookableHooker( 158 | override val logger: XPLogger = XPLogger 159 | ) : OneshotHooker, UnhookableHooker, HookerHelper { 160 | 161 | private val unhooks: ArrayList = ArrayList() 162 | 163 | override fun init(lp: LoadPackageParam) {} 164 | 165 | override fun startHook(lp: LoadPackageParam) { 166 | if (!isInterest(lp)) { 167 | return 168 | } 169 | runCatchingWithLog { 170 | logger.verbose("startHook >>>>") 171 | unhooks.addAll(createHooks(lp)) 172 | logger.verbose("startHook <<<<") 173 | } 174 | } 175 | 176 | override fun startOneshotHook(lp: LoadPackageParam) { 177 | if (!isInterest(lp)) { 178 | return 179 | } 180 | runCatchingWithLog { 181 | logger.verbose("startOneshotHook >>>>") 182 | createOneshotHook(lp) 183 | logger.verbose("startOneshotHook <<<<") 184 | } 185 | } 186 | 187 | abstract fun createOneshotHook(lp: LoadPackageParam) 188 | 189 | abstract fun createHooks(lp: LoadPackageParam): List 190 | 191 | override fun unhook() { 192 | unhooks.forEach { it.unhook() } 193 | } 194 | } -------------------------------------------------------------------------------- /hook/src/main/java/cn/tinyhai/xp/hook/callback/HookCallbacks.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.hook.callback 2 | 3 | import de.robv.android.xposed.XC_MethodHook 4 | 5 | fun beforeMethod(block: (XC_MethodHook.MethodHookParam) -> Unit) = object : XC_MethodHook() { 6 | override fun beforeHookedMethod(param: MethodHookParam) { 7 | block(param) 8 | } 9 | } 10 | 11 | fun afterMethod(block: (XC_MethodHook.MethodHookParam) -> Unit) = object : XC_MethodHook() { 12 | override fun afterHookedMethod(param: MethodHookParam) { 13 | block(param) 14 | } 15 | } -------------------------------------------------------------------------------- /hook/src/main/java/cn/tinyhai/xp/hook/logger/XPLogger.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.hook.logger 2 | 3 | interface XPLogger { 4 | fun info(s: String) 5 | fun debug(s: String) 6 | fun verbose(s: String) 7 | fun error(s: String) 8 | fun error(th: Throwable) 9 | fun error(s: String, th: Throwable) 10 | 11 | companion object : XPLogger { 12 | override fun info(s: String) {} 13 | 14 | override fun debug(s: String) {} 15 | 16 | override fun verbose(s: String) {} 17 | 18 | override fun error(s: String) {} 19 | 20 | override fun error(th: Throwable) {} 21 | 22 | override fun error(s: String, th: Throwable) {} 23 | } 24 | } 25 | 26 | class XPLoggerWrapper(var realLogger: XPLogger = XPLogger) : XPLogger { 27 | override fun info(s: String) { 28 | realLogger.info(s) 29 | } 30 | 31 | override fun debug(s: String) { 32 | realLogger.debug(s) 33 | } 34 | 35 | override fun verbose(s: String) { 36 | realLogger.verbose(s) 37 | } 38 | 39 | override fun error(s: String) { 40 | realLogger.error(s) 41 | } 42 | 43 | override fun error(th: Throwable) { 44 | realLogger.error(th) 45 | } 46 | 47 | override fun error(s: String, th: Throwable) { 48 | realLogger.error(s, th) 49 | } 50 | } -------------------------------------------------------------------------------- /processor/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /processor/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.jetbrainsKotlinJvm) 3 | } 4 | 5 | dependencies { 6 | implementation(project(":hook")) 7 | implementation(libs.kotlinpoet) 8 | implementation(libs.kotlinpoet.ksp) 9 | implementation(libs.xposed.api) 10 | implementation(libs.ksp.api) 11 | } -------------------------------------------------------------------------------- /processor/src/main/java/cn/tinyhai/xp/processor/Processor.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.processor 2 | 3 | import cn.tinyhai.xp.hook.Hooker 4 | import cn.tinyhai.xp.processor.codegen.GenerateHookScope 5 | import cn.tinyhai.xp.processor.codegen.GenerateHookerManager 6 | import cn.tinyhai.xp.processor.parser.HookScopeParser 7 | import com.google.devtools.ksp.processing.* 8 | import com.google.devtools.ksp.symbol.KSAnnotated 9 | import com.squareup.kotlinpoet.asClassName 10 | import com.squareup.kotlinpoet.asTypeName 11 | import com.squareup.kotlinpoet.ksp.writeTo 12 | 13 | class Processor( 14 | private val codeGenerator: CodeGenerator, 15 | private val logger: KSPLogger 16 | ) : SymbolProcessor { 17 | override fun process(resolver: Resolver): List { 18 | logger.info("start >>>>>>>>>>>>>>>>>>>>>>") 19 | val packageName = Hooker::class.asClassName().packageName 20 | val hookeScopeParser = HookScopeParser(resolver, logger) 21 | val allAutoRegisterHookScope = hookeScopeParser.parse().map { hookScopeInfo -> 22 | GenerateHookScope(packageName, hookScopeInfo, logger).start().let { fileSpec -> 23 | fileSpec.writeTo(codeGenerator, Dependencies(true)) 24 | if (hookScopeInfo.autoRegister) { 25 | fileSpec 26 | } else { 27 | null 28 | } 29 | } 30 | }.filterNotNull() 31 | if (allAutoRegisterHookScope.isNotEmpty()) { 32 | val hookerManagerFile = 33 | GenerateHookerManager(allAutoRegisterHookScope, packageName).start() 34 | hookerManagerFile.also { logger.info(it.toString()) }.writeTo(codeGenerator, Dependencies(true)) 35 | } 36 | logger.info("end <<<<<<<<<<<<<<<<<<<<<<<<") 37 | return emptyList() 38 | } 39 | } -------------------------------------------------------------------------------- /processor/src/main/java/cn/tinyhai/xp/processor/ProcessorProvider.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.processor 2 | 3 | import com.google.devtools.ksp.processing.SymbolProcessor 4 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 5 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 6 | 7 | class ProcessorProvider : SymbolProcessorProvider { 8 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { 9 | return Processor( 10 | environment.codeGenerator, 11 | environment.logger 12 | ) 13 | } 14 | } -------------------------------------------------------------------------------- /processor/src/main/java/cn/tinyhai/xp/processor/codegen/CodeGen.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.processor.codegen 2 | 3 | import com.squareup.kotlinpoet.FileSpec 4 | 5 | interface CodeGen { 6 | fun start(): FileSpec 7 | } -------------------------------------------------------------------------------- /processor/src/main/java/cn/tinyhai/xp/processor/codegen/GenerateHookerManager.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.processor.codegen 2 | 3 | import cn.tinyhai.xp.hook.Hooker 4 | import cn.tinyhai.xp.hook.logger.XPLogger 5 | import cn.tinyhai.xp.hook.logger.XPLoggerWrapper 6 | import com.squareup.kotlinpoet.* 7 | import com.squareup.kotlinpoet.MemberName.Companion.member 8 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter 9 | import de.robv.android.xposed.callbacks.XC_LoadPackage 10 | 11 | class GenerateHookerManager( 12 | private val fileSpecs: List, 13 | private val packageName: String, 14 | ) : CodeGen { 15 | override fun start(): FileSpec { 16 | val mutableListOf = MemberName("kotlin.collections", "mutableListOf") 17 | val hookerClassName = Hooker::class.asClassName() 18 | val mutableListWithTypeParam = 19 | ClassName("kotlin.collections", "MutableList").plusParameter(hookerClassName) 20 | val allHookers = 21 | PropertySpec.builder("allHookers", mutableListWithTypeParam, KModifier.PRIVATE) 22 | .initializer("%N<%T>()", mutableListOf, hookerClassName) 23 | .build() 24 | val loggerClassName = XPLogger::class.asClassName() 25 | val loggerParam = ParameterSpec.builder("logger", loggerClassName) 26 | .defaultValue("%T", loggerClassName) 27 | .build() 28 | val loggerWrapperProperty = buildLoggerWrapperProperty(loggerParam) 29 | val loggerProperty = buildLoggerProperty(loggerWrapperProperty) 30 | return FileSpec.builder(packageName, "HookerManager") 31 | .addType( 32 | TypeSpec.classBuilder("HookerManager") 33 | .primaryConstructor( 34 | FunSpec.constructorBuilder().addParameter(loggerParam).build() 35 | ) 36 | .addProperty( 37 | allHookers 38 | ) 39 | .addProperty(loggerWrapperProperty) 40 | .addProperty(loggerProperty) 41 | .addInitializerBlock( 42 | buildInitBlock(allHookers, loggerWrapperProperty, fileSpecs) 43 | ) 44 | .addFunction( 45 | buildStartHook(allHookers, hookerClassName) 46 | ) 47 | .build() 48 | ) 49 | .build() 50 | } 51 | 52 | private fun buildInitBlock( 53 | allHookers: PropertySpec, 54 | loggerWrapper: PropertySpec, 55 | fileSpec: List 56 | ): CodeBlock { 57 | return buildCodeBlock { 58 | fileSpec.forEach { 59 | val implClass = ClassName(it.packageName, it.name) 60 | add("%N.add(%T(%N))\n", allHookers, implClass, loggerWrapper) 61 | } 62 | } 63 | } 64 | 65 | private fun buildStartHook(allHookers: PropertySpec, hookerClassName: ClassName): FunSpec { 66 | val isInterest = hookerClassName.member("isInterest") 67 | val init = hookerClassName.member("init") 68 | val startHook = hookerClassName.member("startHook") 69 | val oneshotHooker = ClassName(hookerClassName.packageName, "OneshotHooker") 70 | val startOneshotHook = oneshotHooker.member("startOneshotHook") 71 | return FunSpec.builder("startHook") 72 | .addParameter("lp", XC_LoadPackage.LoadPackageParam::class) 73 | .addCode( 74 | buildCodeBlock { 75 | beginControlFlow("for (hooker in %N) {", allHookers) 76 | beginControlFlow("if (!hooker.%N(lp)) {", isInterest) 77 | add("continue\n") 78 | endControlFlow() 79 | add("hooker.%N(lp)\n", init) 80 | beginControlFlow("if (hooker is %T) {", oneshotHooker) 81 | add("hooker.%N(lp)\n", startOneshotHook) 82 | endControlFlow() 83 | add("hooker.%N(lp)\n", startHook) 84 | endControlFlow() 85 | } 86 | ) 87 | .build() 88 | } 89 | 90 | private fun buildLoggerWrapperProperty(loggerParam: ParameterSpec): PropertySpec { 91 | val loggerWrapperClass = XPLoggerWrapper::class.asClassName() 92 | return PropertySpec.builder("loggerWrapper", loggerWrapperClass, KModifier.PRIVATE) 93 | .initializer("%T(%N)", loggerWrapperClass, loggerParam) 94 | .build() 95 | } 96 | 97 | private fun buildLoggerProperty(xpLoggerWrapper: PropertySpec): PropertySpec { 98 | val loogerWrapperClass = XPLoggerWrapper::class.asClassName() 99 | val loggerClass = XPLogger::class.asClassName() 100 | val setterParam = ParameterSpec.builder("value", loggerClass).build() 101 | return PropertySpec.builder("logger", loggerClass) 102 | .mutable(true) 103 | .setter( 104 | FunSpec 105 | .setterBuilder() 106 | .addParameter(setterParam) 107 | .addStatement( 108 | "%N.%N = %N", 109 | xpLoggerWrapper, 110 | loogerWrapperClass.member("realLogger"), 111 | setterParam 112 | ) 113 | .build() 114 | ) 115 | .getter( 116 | FunSpec 117 | .getterBuilder() 118 | .addStatement("return %N", xpLoggerWrapper) 119 | .build() 120 | ) 121 | .build() 122 | } 123 | } -------------------------------------------------------------------------------- /processor/src/main/java/cn/tinyhai/xp/processor/codegen/HookScopeInfo.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.processor.codegen 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.MemberName 5 | 6 | data class HookScopeInfo( 7 | val scopeName: String, 8 | val isUnhookable: Boolean, 9 | val hasLogger: Boolean, 10 | val targetPackageName: String, 11 | val targetProcessName: String, 12 | val autoRegister: Boolean, 13 | val hookerGate: MemberName?, 14 | val initiate: MemberName?, 15 | val scopePackageName: String, 16 | val scopeClassName: ClassName?, 17 | val injectMemberName: MemberName?, 18 | val hookerInfos: List 19 | ) -------------------------------------------------------------------------------- /processor/src/main/java/cn/tinyhai/xp/processor/codegen/HookerInfo.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.processor.codegen 2 | 3 | import cn.tinyhai.xp.annotation.HookType 4 | 5 | data class HookerInfo( 6 | val hookerId: String?, 7 | val targetClassName: String, 8 | val targetMethod: String, 9 | val hookerMethod: String, 10 | val isOneshot: Boolean, 11 | val unhookable: Boolean, 12 | val hookType: HookType, 13 | val minSdk: Int, 14 | val maxSdk: Int 15 | ) -------------------------------------------------------------------------------- /processor/src/main/java/cn/tinyhai/xp/processor/parser/HookScopeParser.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.processor.parser 2 | 3 | import cn.tinyhai.xp.annotation.* 4 | import cn.tinyhai.xp.hook.logger.XPLogger 5 | import cn.tinyhai.xp.processor.codegen.HookScopeInfo 6 | import com.google.devtools.ksp.* 7 | import com.google.devtools.ksp.processing.KSPLogger 8 | import com.google.devtools.ksp.processing.Resolver 9 | import com.google.devtools.ksp.symbol.* 10 | import com.squareup.kotlinpoet.MemberName 11 | import com.squareup.kotlinpoet.MemberName.Companion.member 12 | import com.squareup.kotlinpoet.ksp.toClassName 13 | import de.robv.android.xposed.XC_MethodHook 14 | import de.robv.android.xposed.callbacks.XC_LoadPackage 15 | 16 | class HookScopeParser(private val resolver: Resolver, private val logger: KSPLogger) : 17 | Parser { 18 | override fun parse(): List { 19 | return getAllScopeInfo().toList() 20 | } 21 | 22 | private fun getAllScopeInfo(): Sequence { 23 | return getFileScopeInfo() + getClassScopeInfo() 24 | } 25 | 26 | @OptIn(KspExperimental::class) 27 | private fun getFileScopeInfo(): Sequence { 28 | return getFileWithHookScope().map { 29 | val hookScope = it.getAnnotationsByType(HookScope::class).single() 30 | val scopeName = hookScope.scopeName.ifBlank { it.fileName } 31 | var hookerGate: MemberName? = null 32 | var initiate: MemberName? = null 33 | val packageName = it.packageName.getQualifier() 34 | val allFunctions = it.declarations.filterIsInstance().onEach { 35 | if (isHookerGate(it)) { 36 | hookerGate = MemberName(packageName, it.simpleName.getShortName()) 37 | } 38 | if (isInitiate(it)) { 39 | initiate = MemberName(packageName, it.simpleName.getShortName()) 40 | } 41 | } 42 | val injectHookerProperty = 43 | it.declarations.filterIsInstance().filter { it.isPublic() } 44 | .filter { it.isAnnotationPresent(InjectHooker::class) } 45 | .firstOrNull() 46 | val injectHookerMemberName = injectHookerProperty?.let { 47 | MemberName(it.packageName.getQualifier(), it.simpleName.getShortName()) 48 | } 49 | val hookerInfos = 50 | HookerInfoParser(hookScope.isUnhookable, allFunctions, resolver).parse() 51 | HookScopeInfo( 52 | scopeName = scopeName, 53 | isUnhookable = hookScope.isUnhookable, 54 | hasLogger = false, 55 | targetPackageName = hookScope.targetPackageName, 56 | targetProcessName = hookScope.targetProcessName, 57 | autoRegister = hookScope.autoRegister, 58 | hookerGate = hookerGate, 59 | initiate = initiate, 60 | scopePackageName = packageName, 61 | scopeClassName = null, 62 | injectMemberName = injectHookerMemberName, 63 | hookerInfos = hookerInfos 64 | ) 65 | } 66 | } 67 | 68 | @OptIn(KspExperimental::class) 69 | private fun getClassScopeInfo(): Sequence { 70 | return resolver.getClassWithHookScope().map { 71 | val hookScope = it.getAnnotationsByType(HookScope::class).single() 72 | val scopeName = hookScope.scopeName.ifBlank { it.simpleName.getShortName() } 73 | var hookerGate: MemberName? = null 74 | var initiate: MemberName? = null 75 | val scopeClassName = it.toClassName() 76 | val allFunctions = it.getDeclaredFunctions().onEach { 77 | if (isHookerGate(it)) { 78 | hookerGate = scopeClassName.member(it.simpleName.getShortName()) 79 | } 80 | if (isInitiate(it)) { 81 | initiate = scopeClassName.member(it.simpleName.getShortName()) 82 | } 83 | } 84 | val hookerInfos = 85 | HookerInfoParser(hookScope.isUnhookable, allFunctions, resolver).parse() 86 | val hasLogger = it.primaryConstructor?.let { hasLoggerParam(it) } ?: false 87 | 88 | val injectHookerProperty = it.getDeclaredProperties().filter { it.isPublic() } 89 | .filter { it.isAnnotationPresent(InjectHooker::class) } 90 | .firstOrNull() 91 | val injectHookerMemberName = injectHookerProperty?.let { 92 | scopeClassName.member(it.simpleName.getShortName()) 93 | } 94 | 95 | HookScopeInfo( 96 | scopeName = scopeName, 97 | isUnhookable = hookScope.isUnhookable, 98 | hasLogger = hasLogger, 99 | targetPackageName = hookScope.targetPackageName, 100 | targetProcessName = hookScope.targetProcessName, 101 | autoRegister = hookScope.autoRegister, 102 | hookerGate = hookerGate, 103 | initiate = initiate, 104 | scopePackageName = scopeClassName.packageName, 105 | scopeClassName = scopeClassName, 106 | injectMemberName = injectHookerMemberName, 107 | hookerInfos = hookerInfos 108 | ) 109 | } 110 | } 111 | 112 | @OptIn(KspExperimental::class) 113 | private fun isHookerGate(function: KSFunctionDeclaration): Boolean { 114 | if (!function.isAnnotationPresent(HookerGate::class)) { 115 | return false 116 | } 117 | val returnType = function.returnType?.resolve() 118 | if (returnType?.isAssignableFrom(resolver) != true) { 119 | return false 120 | } 121 | val parameterSize = function.parameters.size 122 | if (parameterSize != 1) { 123 | return false 124 | } 125 | val parameterType = function.parameters[0].type.resolve() 126 | if (!parameterType.isAssignableFrom(resolver)) { 127 | return false 128 | } 129 | return true 130 | } 131 | 132 | @OptIn(KspExperimental::class) 133 | private fun isInitiate(function: KSFunctionDeclaration): Boolean { 134 | if (!function.isAnnotationPresent(Initiate::class)) { 135 | return false 136 | } 137 | val parameterSize = function.parameters.size 138 | if (parameterSize != 1) { 139 | return false 140 | } 141 | val parameterType = function.parameters[0].type.resolve() 142 | if (!parameterType.isAssignableFrom(resolver)) { 143 | return false 144 | } 145 | return true 146 | } 147 | 148 | private fun hasLoggerParam(function: KSFunctionDeclaration): Boolean { 149 | if (!function.isPublic()) { 150 | return false 151 | } 152 | if (function.parameters.size != 1) { 153 | return false 154 | } 155 | val param = function.parameters[0].type.resolve() 156 | return param.isAssignableFrom(resolver) 157 | } 158 | 159 | private inline fun KSType.isAssignableFrom(resolver: Resolver): Boolean { 160 | val classDeclaration = requireNotNull(resolver.getClassDeclarationByName()) { 161 | "Unable to resolve ${KSClassDeclaration::class.simpleName} for type ${T::class.simpleName}" 162 | } 163 | return isAssignableFrom(classDeclaration.asStarProjectedType()) 164 | } 165 | 166 | private fun Resolver.getClassWithHookScope(): Sequence { 167 | return getSymbolsWithAnnotation(HookScope::class.qualifiedName.orEmpty()).filterIsInstance() 168 | .filter { 169 | it.classKind == ClassKind.CLASS 170 | } 171 | .filter { 172 | !it.modifiers.contains(Modifier.ABSTRACT) 173 | } 174 | } 175 | 176 | private fun getFileWithHookScope() = 177 | resolver.getSymbolsWithAnnotation(MethodHooker::class.qualifiedName.orEmpty()) 178 | .filterIsInstance() 179 | } -------------------------------------------------------------------------------- /processor/src/main/java/cn/tinyhai/xp/processor/parser/HookerInfoParser.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.processor.parser 2 | 3 | import cn.tinyhai.xp.annotation.HookType 4 | import cn.tinyhai.xp.annotation.HookerId 5 | import cn.tinyhai.xp.annotation.MethodHooker 6 | import cn.tinyhai.xp.annotation.Oneshot 7 | import cn.tinyhai.xp.processor.codegen.HookerInfo 8 | import com.google.devtools.ksp.KspExperimental 9 | import com.google.devtools.ksp.getAnnotationsByType 10 | import com.google.devtools.ksp.getClassDeclarationByName 11 | import com.google.devtools.ksp.isAnnotationPresent 12 | import com.google.devtools.ksp.processing.Resolver 13 | import com.google.devtools.ksp.symbol.KSClassDeclaration 14 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 15 | import com.google.devtools.ksp.symbol.KSType 16 | import de.robv.android.xposed.XC_MethodHook 17 | 18 | class HookerInfoParser( 19 | private val isUnhookable: Boolean, 20 | private val functions: Sequence, 21 | private val resolver: Resolver, 22 | ) : Parser { 23 | @OptIn(KspExperimental::class) 24 | override fun parse(): List { 25 | return functions.filterWithMethodHooker().map { 26 | val isOneshot = !isUnhookable || it.isAnnotationPresent(Oneshot::class) 27 | val unhookable = 28 | it.getAnnotationsByType(Oneshot::class).firstOrNull()?.unhookable ?: false 29 | it.ensureValid(unhookable) 30 | val hookerId = it.getAnnotationsByType(HookerId::class).firstOrNull()?.id 31 | val methodHooker = it.getAnnotationsByType(MethodHooker::class).single() 32 | HookerInfo( 33 | hookerId = hookerId, 34 | targetClassName = methodHooker.className, 35 | targetMethod = methodHooker.methodName, 36 | hookerMethod = it.simpleName.getShortName(), 37 | isOneshot = isOneshot, 38 | unhookable = unhookable, 39 | hookType = methodHooker.hookType, 40 | minSdk = methodHooker.minSdkInclusive, 41 | maxSdk = methodHooker.maxSdkExclusive 42 | ) 43 | }.toList() 44 | } 45 | 46 | @OptIn(KspExperimental::class) 47 | private fun Sequence.filterWithMethodHooker() = filter { 48 | it.isAnnotationPresent(MethodHooker::class) 49 | } 50 | 51 | private inline fun KSType.isAssignableFrom(resolver: Resolver): Boolean { 52 | val classDeclaration = requireNotNull(resolver.getClassDeclarationByName()) { 53 | "Unable to resolve ${KSClassDeclaration::class.simpleName} for type ${T::class.simpleName}" 54 | } 55 | return isAssignableFrom(classDeclaration.asStarProjectedType()) 56 | } 57 | 58 | private fun KSFunctionDeclaration.ensureValid(unhookable: Boolean) { 59 | val parameterSize = parameters.size 60 | when { 61 | unhookable && parameterSize == 2 -> { 62 | val paramType = parameters[0].type.resolve() 63 | val unhookType = parameters[1].type.resolve() 64 | if (paramType.isAssignableFrom(resolver) 65 | && unhookType.isAssignableFrom<() -> Unit>(resolver) 66 | ) { 67 | return 68 | } 69 | } 70 | 71 | parameterSize == 1 -> { 72 | val paramType = parameters[0].type.resolve() 73 | if (paramType.isAssignableFrom(resolver)) { 74 | return 75 | } 76 | } 77 | 78 | else -> { 79 | throw RuntimeException("${qualifiedName?.getQualifier()} is invalid. unhookable: $unhookable") 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /processor/src/main/java/cn/tinyhai/xp/processor/parser/Parser.kt: -------------------------------------------------------------------------------- 1 | package cn.tinyhai.xp.processor.parser 2 | 3 | interface Parser { 4 | fun parse(): List 5 | } -------------------------------------------------------------------------------- /processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider: -------------------------------------------------------------------------------- 1 | cn.tinyhai.xp.processor.ProcessorProvider -------------------------------------------------------------------------------- /screenshots/screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/screenshots/screenshot1.jpg -------------------------------------------------------------------------------- /screenshots/screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/screenshots/screenshot2.jpg -------------------------------------------------------------------------------- /screenshots/screenshot3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/screenshots/screenshot3.jpg -------------------------------------------------------------------------------- /screenshots/screenshot4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyHai/BanUninstall/b279b7070b4db2a9c810ac7becde4671cc08861e/screenshots/screenshot4.jpg -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | repositories { 17 | google() 18 | mavenCentral() 19 | maven("https://jitpack.io") 20 | maven("https://api.xposed.info") 21 | } 22 | } 23 | 24 | 25 | rootProject.name = "BanUninstall" 26 | include(":app") 27 | include(":hiddenApi") 28 | include(":processor") 29 | include(":hook") --------------------------------------------------------------------------------