├── .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")
--------------------------------------------------------------------------------