├── .github
└── dependabot.yml
├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── assets
│ ├── qq.json
│ ├── wechat.json
│ └── xposed_init
│ ├── java
│ └── me
│ │ └── kyuubiran
│ │ └── qqcleanerlite
│ │ ├── MainHook.kt
│ │ ├── data
│ │ └── CleanData.kt
│ │ ├── dialog
│ │ ├── MainConfigDialog.kt
│ │ ├── ModifyConfigDialog.kt
│ │ └── ModuleDialog.kt
│ │ ├── hook
│ │ ├── AppHook.kt
│ │ ├── BaseHook.kt
│ │ └── EntryHook.kt
│ │ └── util
│ │ ├── CleanManager.kt
│ │ ├── ConfigManager.kt
│ │ ├── PathParser.kt
│ │ ├── Shared.kt
│ │ ├── Utils.kt
│ │ └── path
│ │ ├── CommonPath.kt
│ │ ├── QQPath.kt
│ │ └── WeChatPath.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.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── values
│ ├── arrays.xml
│ ├── colors.xml
│ └── strings.xml
│ └── xml
│ ├── config_main_prefs.xml
│ ├── config_modify_prefs.xml
│ └── setting_dialog_prefs.xml
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gradle"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 | /app/release/
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # QQCleaner Lite
2 | 轻量版本的[瘦身模块](https://github.com/KitsunePie/QQCleaner)
3 |
4 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | }
5 |
6 | android {
7 | compileSdk = 32
8 |
9 | defaultConfig {
10 | applicationId = "me.kyuubiran.qqcleanerlite"
11 | minSdk = 24
12 | targetSdk = 32
13 | versionCode = 1
14 | versionName = "1.0"
15 | }
16 |
17 | buildTypes {
18 | named("release") {
19 | isShrinkResources = true
20 | isMinifyEnabled = true
21 | proguardFiles("proguard-rules.pro")
22 | }
23 | }
24 |
25 | androidResources {
26 | additionalParameters("--allow-reserved-package-id", "--package-id", "0x48")
27 | }
28 |
29 | compileOptions {
30 | sourceCompatibility = JavaVersion.VERSION_11
31 | targetCompatibility = JavaVersion.VERSION_11
32 | }
33 | kotlinOptions {
34 | jvmTarget = JavaVersion.VERSION_11.toString()
35 | }
36 | }
37 |
38 | dependencies {
39 | implementation("com.github.kyuubiran:EzXHelper:1.0.3")
40 | compileOnly("de.robv.android.xposed:api:82")
41 | }
42 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -keep class me.kyuubiran.qqcleanerlite.MainHook
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
12 |
15 |
18 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/assets/qq.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "默认配置",
3 | "author": "KyuubiRan",
4 | "enable": true,
5 | "hostApp": "qq/tim",
6 | "content": [
7 | {
8 | "title": "缓存",
9 | "enable": true,
10 | "path": [
11 | {
12 | "prefix": "!PublicDataDir",
13 | "suffix": "/Tencent/MobileQQ/diskcache"
14 | },
15 | {
16 | "prefix": "!PublicDataDir",
17 | "suffix": "/Tencent/MobileQQ/Scribble/ScribbleCache"
18 | }
19 | ]
20 | },
21 | {
22 | "title": "图片",
23 | "enable": true,
24 | "path": [
25 | {
26 | "prefix": "!PublicDataDir",
27 | "suffix": "/Tencent/MobileQQ/photo"
28 | },
29 | {
30 | "prefix": "!PublicDataDir",
31 | "suffix": "/Tencent/MobileQQ/chatpic"
32 | },
33 | {
34 | "prefix": "!PublicDataDir",
35 | "suffix": "/Tencent/MobileQQ/thumb"
36 | },
37 | {
38 | "prefix": "!PublicDataDir",
39 | "suffix": "/Tencent/MobileQQ/hotpic"
40 | },
41 | {
42 | "prefix": "!PublicDataDir",
43 | "suffix": "/QQ_Images/QQEditPic"
44 | }
45 | ]
46 | },
47 | {
48 | "title": "短视频",
49 | "enable": true,
50 | "path": [
51 | {
52 | "prefix": "!PublicDataDir",
53 | "suffix": "/Tencent/MobileQQ/shortvideo"
54 | },
55 | {
56 | "prefix": "!PublicDataDir",
57 | "suffix": "/files/VideoCache"
58 | },
59 | {
60 | "prefix": "!PublicDataDir",
61 | "suffix": "/files/video_story"
62 | }
63 | ]
64 | },
65 | {
66 | "title": "DIY名片",
67 | "enable": true,
68 | "path": [
69 | {
70 | "prefix": "!PublicDataDir",
71 | "suffix": "/Tencent/MobileQQ/.apollo"
72 | },
73 | {
74 | "prefix": "!PublicDataDir",
75 | "suffix": "/Tencent/MobileQQ/vas/lottie"
76 | }
77 | ]
78 | },
79 | {
80 | "title": "广告",
81 | "enable": true,
82 | "path": [
83 | {
84 | "prefix": "!PublicDataDir",
85 | "suffix": "/Tencent/MobileQQ/pddata"
86 | },
87 | {
88 | "prefix": "!PublicDataDir",
89 | "suffix": "/Tencent/MobileQQ/qbosssplahAD"
90 | }
91 | ]
92 | },
93 | {
94 | "title": "头像",
95 | "enable": false,
96 | "path": [
97 | {
98 | "prefix": "!PublicDataDir",
99 | "suffix": "/Tencent/MobileQQ/head"
100 | },
101 | {
102 | "prefix": "!TencentDir",
103 | "suffix": "/MobileQQ/head"
104 | }
105 | ]
106 | },
107 | {
108 | "title": "挂件",
109 | "enable": false,
110 | "path": [
111 | {
112 | "prefix": "!PublicDataDir",
113 | "suffix": "/Tencent/MobileQQ/.pendant"
114 | }
115 | ]
116 | },
117 | {
118 | "title": "个人主页背景",
119 | "enable": false,
120 | "path": [
121 | {
122 | "prefix": "!PublicDataDir",
123 | "suffix": "/Tencent/MobileQQ/.profilecard"
124 | }
125 | ]
126 | },
127 | {
128 | "title": "视频通话背景",
129 | "enable": false,
130 | "path": [
131 | {
132 | "prefix": "!PublicDataDir",
133 | "suffix": "/Tencent/MobileQQ/funcall"
134 | }
135 | ]
136 | },
137 | {
138 | "title": "小程序",
139 | "enable": false,
140 | "path": [
141 | {
142 | "prefix": "!PublicDataDir",
143 | "suffix": "/Tencent/MobileQQ/mini"
144 | }
145 | ]
146 | },
147 | {
148 | "title": "字体",
149 | "enable": false,
150 | "path": [
151 | {
152 | "prefix": "!PublicDataDir",
153 | "suffix": "/Tencent/MobileQQ/.hiboom_font"
154 | },
155 | {
156 | "prefix": "!PublicDataDir",
157 | "suffix": "/Tencent/MobileQQ/.font_info"
158 | }
159 | ]
160 | },
161 | {
162 | "title": "礼物",
163 | "enable": false,
164 | "path": [
165 | {
166 | "prefix": "!PublicDataDir",
167 | "suffix": "/Tencent/MobileQQ/.gift"
168 | }
169 | ]
170 | },
171 | {
172 | "title": "戳一戳",
173 | "enable": false,
174 | "path": [
175 | {
176 | "prefix": "!PublicDataDir",
177 | "suffix": "/Tencent/MobileQQ/.vaspoke"
178 | },
179 | {
180 | "prefix": "!PublicDataDir",
181 | "suffix": "/Tencent/MobileQQ/.newpoke"
182 | },
183 | {
184 | "prefix": "!PublicDataDir",
185 | "suffix": "/Tencent/MobileQQ/poke"
186 | }
187 | ]
188 | },
189 | {
190 | "title": "进场特效",
191 | "enable": false,
192 | "path": [
193 | {
194 | "prefix": "!PublicDataDir",
195 | "suffix": "/Tencent/MobileQQ/.troop/enter_effects"
196 | }
197 | ]
198 | },
199 | {
200 | "title": "聊天表情",
201 | "enable": false,
202 | "path": [
203 | {
204 | "prefix": "!PublicDataDir",
205 | "suffix": "/Tencent/MobileQQ/.emotionsm"
206 | }
207 | ]
208 | },
209 | {
210 | "title": "VIP图标",
211 | "enable": false,
212 | "path": [
213 | {
214 | "prefix": "!PublicDataDir",
215 | "suffix": "/Tencent/MobileQQ/.vipicon"
216 | }
217 | ]
218 | },
219 | {
220 | "title": "斗图",
221 | "enable": false,
222 | "path": [
223 | {
224 | "prefix": "!PublicDataDir",
225 | "suffix": "/Tencent/MobileQQ/DoutuRes"
226 | },
227 | {
228 | "prefix": "!PublicDataDir",
229 | "suffix": "/Tencent/MobileQQ/.sticker_recommended_pics"
230 | },
231 | {
232 | "prefix": "!PublicDataDir",
233 | "suffix": "/Tencent/MobileQQ/pe"
234 | }
235 | ]
236 | },
237 | {
238 | "title": "日志",
239 | "enable": false,
240 | "path": [
241 | {
242 | "prefix": "!PublicDataDir",
243 | "suffix": "/files/tbslog"
244 | },
245 | {
246 | "prefix": "!PublicDataDir",
247 | "suffix": "/files/onelong"
248 | },
249 | {
250 | "prefix": "!PublicDataDir",
251 | "suffix": "/files/tencent/tbs_common_log"
252 | },
253 | {
254 | "prefix": "!PublicDataDir",
255 | "suffix": "/files/tencent/tbs_live_log"
256 | }
257 | ]
258 | },
259 | {
260 | "title": "接收文件缓存",
261 | "enable": false,
262 | "path": [
263 | {
264 | "prefix": "!PublicDataDir",
265 | "suffix": "/Tencent/QQfile_recv/trooptmp"
266 | },
267 | {
268 | "prefix": "!PublicDataDir",
269 | "suffix": "/Tencent/QQfile_recv/tmp"
270 | },
271 | {
272 | "prefix": "!PublicDataDir",
273 | "suffix": "/Tencent/QQfile_recv/thumbnails"
274 | }
275 | ]
276 | },
277 | {
278 | "title": "调试数据",
279 | "enable": false,
280 | "path": [
281 | {
282 | "prefix": "!PublicDataDir",
283 | "suffix": "/avdebug"
284 | }
285 | ]
286 | },
287 | {
288 | "title": "其他",
289 | "enable": false,
290 | "path": [
291 | {
292 | "prefix": "!PublicDataDir",
293 | "suffix": "/Tencent/MobileQQ/qav"
294 | },
295 | {
296 | "prefix": "!PublicDataDir",
297 | "suffix": "/Tencent/MobileQQ/qqmusic"
298 | },
299 | {
300 | "prefix": "!PublicDataDir",
301 | "suffix": "/Tencent/MobileQQ/pddata"
302 | },
303 | {
304 | "prefix": "!PublicDataDir",
305 | "suffix": "/files/tbs"
306 | },
307 | {
308 | "prefix": "!TencentDir",
309 | "suffix": "/TMAssistantSDK"
310 | }
311 | ]
312 | },
313 | {
314 | "title": "虚幻引擎",
315 | "enable": false,
316 | "path": [
317 | {
318 | "prefix": "!PublicDataDir",
319 | "suffix": "/files/UE4Game"
320 | }
321 | ]
322 | },
323 | {
324 | "title": "超级QQ秀",
325 | "enable": false,
326 | "path": [
327 | {
328 | "prefix": "!PublicDataDir",
329 | "suffix": "/files/QQShowDownload"
330 | },
331 | {
332 | "prefix": "!PublicDataDir",
333 | "suffix": "/files/zootopia_download"
334 | }
335 | ]
336 | }
337 | ]
338 | }
--------------------------------------------------------------------------------
/app/src/main/assets/wechat.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "默认配置",
3 | "author": "KyuubiRan",
4 | "enable": true,
5 | "hostApp": "wechat",
6 | "content": [
7 | {
8 | "title": "缓存",
9 | "enable": true,
10 | "path": [
11 | {
12 | "prefix": "!PublicDataDir",
13 | "suffix": "/cache"
14 | },
15 | {
16 | "prefix": "!PublicDataDir",
17 | "suffix": "/MicroMsg/CDNTemp"
18 | },
19 | {
20 | "prefix": "!PublicDataDir",
21 | "suffix": "/MicroMsg/FailMsgFileCache"
22 | },
23 | {
24 | "prefix": "!PrivateDataDir",
25 | "suffix": "/cache/temp"
26 | },
27 | {
28 | "prefix": "!PublicUserDataDir",
29 | "suffix": "/webcanvascache"
30 | }
31 | ]
32 | },
33 | {
34 | "title": "图片",
35 | "enable": false,
36 | "path": [
37 | {
38 | "prefix": "!PrivateUserDataDir",
39 | "suffix": "/image2"
40 | },
41 | {
42 | "prefix": "!PrivateDataDir",
43 | "suffix": "/MicroMsg/tmpScanLicense"
44 | }
45 | ]
46 | },
47 | {
48 | "title": "视频",
49 | "enable": false,
50 | "path": [
51 | {
52 | "prefix": "!PublicUserDataDir",
53 | "suffix": "/video"
54 | },
55 | {
56 | "prefix": "!PrivateDataDir",
57 | "suffix": "/cache/mv_video"
58 | }
59 | ]
60 | },
61 | {
62 | "title": "头像",
63 | "enable": false,
64 | "path": [
65 | {
66 | "prefix": "!PrivateUserDataDir",
67 | "suffix": "/avatar"
68 | }
69 | ]
70 | },
71 | {
72 | "title": "小程序",
73 | "enable": false,
74 | "path": [
75 | {
76 | "prefix": "!PrivateUserDataDir",
77 | "suffix": "/appbrand"
78 | },
79 | {
80 | "prefix": "!PrivateDataDir",
81 | "suffix": "/MicroMsg/appbrand"
82 | }
83 | ]
84 | },
85 | {
86 | "title": "红包皮肤",
87 | "enable": false,
88 | "path": [
89 | {
90 | "prefix": "!PrivateDataDir",
91 | "suffix": "/MicroMsg/luckymoney"
92 | }
93 | ]
94 | },
95 | {
96 | "title": "日志",
97 | "enable": false,
98 | "path": [
99 | {
100 | "prefix": "!PublicDataDir",
101 | "suffix": "/MicroMsg/crash"
102 | },
103 | {
104 | "prefix": "!PublicDataDir",
105 | "suffix": "/MicroMsg/xlog"
106 | },
107 | {
108 | "prefix": "!PublicDataDir",
109 | "suffix": "/files/onelog"
110 | },
111 | {
112 | "prefix": "!PublicDataDir",
113 | "suffix": "/files/tbslog"
114 | },
115 | {
116 | "prefix": "!PublicDataDir",
117 | "suffix": "/files/Tencent/tbs_common_log"
118 | },
119 | {
120 | "prefix": "!PublicDataDir",
121 | "suffix": "/files/Tencent/tbs_live_log"
122 | },
123 | {
124 | "prefix": "!PrivateDataDir",
125 | "suffix": "/MicroMsg/crash"
126 | },
127 | {
128 | "prefix": "!PrivateDataDir",
129 | "suffix": "/files/xlog"
130 | }
131 | ]
132 | },
133 | {
134 | "title": "资源更新",
135 | "enable": false,
136 | "path": [
137 | {
138 | "prefix": "!PublicDataDir",
139 | "suffix": "/MicroMsg/CheckResUpdate"
140 | }
141 | ]
142 | },
143 | {
144 | "title": "X5内核",
145 | "enable": false,
146 | "path": [
147 | {
148 | "prefix": "!PublicDataDir",
149 | "suffix": "/app_tbs"
150 | },
151 | {
152 | "prefix": "!PrivateDataDir",
153 | "suffix": "/app_tbs"
154 | },
155 | {
156 | "prefix": "!PublicDataDir",
157 | "suffix": "/app_tbs_64"
158 | },
159 | {
160 | "prefix": "!PrivateDataDir",
161 | "suffix": "/app_tbs_64"
162 | },
163 | {
164 | "prefix": "!PublicDataDir",
165 | "suffix": "/app_x5webview"
166 | },
167 | {
168 | "prefix": "!PrivateDataDir",
169 | "suffix": "/app_x5webview"
170 | }
171 | ]
172 | }
173 | ]
174 | }
--------------------------------------------------------------------------------
/app/src/main/assets/xposed_init:
--------------------------------------------------------------------------------
1 | me.kyuubiran.qqcleanerlite.MainHook
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/MainHook.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite
2 |
3 | import com.github.kyuubiran.ezxhelper.init.EzXHelperInit
4 | import com.github.kyuubiran.ezxhelper.utils.Log
5 | import com.github.kyuubiran.ezxhelper.utils.Log.logexIfThrow
6 | import de.robv.android.xposed.IXposedHookLoadPackage
7 | import de.robv.android.xposed.IXposedHookZygoteInit
8 | import de.robv.android.xposed.callbacks.XC_LoadPackage
9 | import me.kyuubiran.qqcleanerlite.hook.AppHook
10 | import me.kyuubiran.qqcleanerlite.hook.BaseHook
11 | import me.kyuubiran.qqcleanerlite.hook.EntryHook
12 | import me.kyuubiran.qqcleanerlite.util.HOST_APP
13 | import me.kyuubiran.qqcleanerlite.util.HostAppType
14 |
15 | private val PACKAGE_NAME_HOOKED = listOf("com.tencent.mobileqq", "com.tencent.tim", "com.tencent.mm")
16 | private const val TAG = "QQCleanerLite"
17 |
18 | class MainHook : IXposedHookLoadPackage, IXposedHookZygoteInit {
19 | override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
20 | if (lpparam.packageName !in PACKAGE_NAME_HOOKED) return
21 | if (lpparam.packageName != lpparam.processName) return
22 | HOST_APP = when (lpparam.packageName) {
23 | "com.tencent.mobileqq" -> HostAppType.QQ
24 | "com.tencent.mm" -> HostAppType.WECHAT
25 | "com.tencent.tim" -> HostAppType.TIM
26 | else -> return
27 | }
28 | EzXHelperInit.initHandleLoadPackage(lpparam)
29 | EzXHelperInit.setLogTag(TAG)
30 | EzXHelperInit.setToastTag(TAG)
31 | initHooks(AppHook, EntryHook)
32 | }
33 |
34 | override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
35 | EzXHelperInit.initZygote(startupParam)
36 | }
37 |
38 | private fun initHooks(vararg hook: BaseHook) {
39 | hook.forEach {
40 | runCatching {
41 | if (it.isInit) return@forEach
42 | it.init()
43 | it.isInit = true
44 | Log.i("Inited hook: ${it.javaClass.simpleName}")
45 | }.logexIfThrow("Failed init hook: ${it.javaClass.simpleName}")
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/data/CleanData.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.data
2 |
3 | import android.content.ClipData
4 | import android.content.ClipboardManager
5 | import android.content.Context
6 | import com.github.kyuubiran.ezxhelper.init.InitFields.appContext
7 | import com.github.kyuubiran.ezxhelper.init.InitFields.moduleRes
8 | import com.github.kyuubiran.ezxhelper.utils.Log
9 | import com.github.kyuubiran.ezxhelper.utils.Log.logeIfThrow
10 | import com.github.kyuubiran.ezxhelper.utils.getBooleanOrDefault
11 | import com.github.kyuubiran.ezxhelper.utils.getJSONArrayOrEmpty
12 | import com.github.kyuubiran.ezxhelper.utils.getStringOrDefault
13 | import me.kyuubiran.qqcleanerlite.util.CleanManager
14 | import me.kyuubiran.qqcleanerlite.util.CleanManager.getConfigDir
15 | import me.kyuubiran.qqcleanerlite.util.CleanManager.pool
16 | import me.kyuubiran.qqcleanerlite.util.HOST_APP
17 | import me.kyuubiran.qqcleanerlite.util.isQqOrTim
18 | import me.kyuubiran.qqcleanerlite.util.validFor
19 | import org.json.JSONObject
20 | import java.io.BufferedReader
21 | import java.io.File
22 | import java.io.InputStreamReader
23 | import java.io.Serializable
24 | import java.net.HttpURLConnection
25 | import java.net.URL
26 |
27 |
28 | class CleanData(private val jsonObject: JSONObject) : Serializable, Cloneable {
29 |
30 | class PathData(private val jsonObject: JSONObject) {
31 |
32 | class Path {
33 | private val jsonObject: JSONObject
34 |
35 | constructor(jsonObject: JSONObject) {
36 | this.jsonObject = jsonObject
37 | }
38 |
39 | constructor(jsonString: String) : this(JSONObject(jsonString))
40 |
41 | constructor(prefix: String, suffix: String) {
42 | this.jsonObject = JSONObject().put("prefix", prefix).put("suffix", suffix)
43 | }
44 |
45 | var prefix: String
46 | set(value) {
47 | jsonObject.put("prefix", value)
48 | }
49 | get() = jsonObject.getStringOrDefault("prefix")
50 |
51 | var suffix: String
52 | set(value) {
53 | jsonObject.put("suffix", value)
54 | }
55 | get() = jsonObject.getStringOrDefault("suffix")
56 |
57 | override fun toString(): String {
58 | return jsonObject.toString()
59 | }
60 |
61 | fun toFormatString(indentSpaces: Int = 2): String {
62 | return jsonObject.toString(indentSpaces)
63 | }
64 | }
65 |
66 | // 标题
67 | var title: String
68 | set(value) {
69 | jsonObject.put("title", value)
70 | }
71 | get() = jsonObject.getStringOrDefault("title", "一个没有名字的配置文件")
72 |
73 | // 是否启用
74 | var enable: Boolean
75 | set(value) {
76 | jsonObject.put("enable", value)
77 | }
78 | get() = jsonObject.getBooleanOrDefault("enable", false)
79 |
80 | // 路径
81 | val pathList = jsonObject.getJSONArrayOrEmpty("path").run {
82 | Log.i("Load path list of $title")
83 | arrayListOf().apply {
84 | for (i in 0 until this@run.length()) {
85 | runCatching {
86 | add(Path(this@run.getJSONObject(i)))
87 | }.logeIfThrow("Load path list of $title failed") {
88 | enable = false
89 | Log.toast("$title 加载失败,请检查配置文件是否合法!")
90 | return@apply
91 | }
92 | }
93 | }
94 | }
95 |
96 | // 添加路径
97 | fun addPath(path: Path) {
98 | pathList.add(path)
99 | }
100 |
101 | //删除路径
102 | fun removePath(idx: Int) = runCatching {
103 | pathList.removeAt(idx)
104 | }.logeIfThrow()
105 |
106 | //删除路径
107 | fun removePath(path: Path) {
108 | pathList.remove(path)
109 | }
110 |
111 | override fun toString(): String = jsonObject.toString()
112 |
113 | fun toFormatString(indentSpaces: Int = 2): String = jsonObject.toString(indentSpaces)
114 | }
115 |
116 | private var file: File? = null
117 |
118 | // 配置文件标题
119 | var title: String
120 | set(value) {
121 | jsonObject.put("title", value)
122 | }
123 | get() = jsonObject.getStringOrDefault("title", "一个没有名字的配置文件")
124 |
125 | // 作者
126 | var author: String
127 | set(value) {
128 | jsonObject.put("author", value)
129 | }
130 | get() = jsonObject.getStringOrDefault("author", "无名氏")
131 |
132 | // 是否启用
133 | var enable: Boolean
134 | set(value) {
135 | jsonObject.put("enable", value)
136 | }
137 | get() = jsonObject.getBooleanOrDefault("enable", false)
138 |
139 | // 宿主类型
140 | var hostApp: String
141 | set(value) {
142 | jsonObject.put("hostApp", value)
143 | }
144 | get() = jsonObject.getStringOrDefault("hostApp")
145 |
146 | val valid: Boolean
147 | get() = HOST_APP.validFor(hostApp)
148 |
149 | // 内容
150 | val content = jsonObject.getJSONArrayOrEmpty("content").run {
151 | arrayListOf().apply {
152 | for (i in 0 until this@run.length()) {
153 | add(PathData(this@run.getJSONObject(i)))
154 | }
155 | }
156 | }
157 |
158 | fun addContent(pathData: PathData) {
159 | content.add(pathData)
160 | }
161 |
162 | fun removeContent(idx: Int) {
163 | content.removeAt(idx)
164 | }
165 |
166 | fun removeContent(pathData: PathData) {
167 | content.remove(pathData)
168 | }
169 |
170 | override fun toString(): String {
171 | return jsonObject.toString()
172 | }
173 |
174 | /**
175 | * 格式化的JSONString
176 | * @param indentSpaces 缩进
177 | */
178 | fun toFormatString(indentSpaces: Int = 2): String {
179 | return jsonObject.toString(indentSpaces)
180 | }
181 |
182 | /**
183 | * 复制到剪切板
184 | */
185 | fun exportToClipboard() {
186 | (appContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).run {
187 | setPrimaryClip(ClipData.newPlainText(title, toFormatString()))
188 | }
189 | }
190 |
191 | /**
192 | * 导出配置文件到下载目录
193 | */
194 | fun exportToDownload() {
195 | val f = File("/storage/emulated/0/Download/${this.title}.json")
196 | if (!f.exists()) f.createNewFile()
197 | f.writeText(this.toFormatString())
198 | }
199 |
200 | /**
201 | * 将配置文件推至队列执行
202 | */
203 | fun pushToExecutionQueue(showToast: Boolean = true, showFinishedToast: Boolean = false) {
204 | CleanManager.execute(this, showToast, true)
205 | if (showToast && showFinishedToast) pool.execute { Log.toast("执行完毕") }
206 | }
207 |
208 | /**
209 | * 保存配置文件 一般在返回的时候调用
210 | */
211 | @Synchronized
212 | fun save() {
213 | file?.let {
214 | if (!it.exists()) it.createNewFile()
215 | it.writeText(toFormatString())
216 | return
217 | }
218 | file = File("${getConfigDir().path}/${title}.json").apply {
219 | if (!exists()) createNewFile()
220 | writeText(toFormatString())
221 | }
222 | }
223 |
224 | /**
225 | * 删除配置文件
226 | */
227 | @Synchronized
228 | fun delete() {
229 | file?.let {
230 | if (it.exists()) it.delete()
231 | }
232 | }
233 |
234 | companion object {
235 | @JvmStatic
236 | fun fromJson(jsonString: String): CleanData {
237 | return CleanData(JSONObject(jsonString))
238 | }
239 |
240 | @JvmStatic
241 | fun fromClipboard(alsoSave: Boolean = true): CleanData? {
242 | (appContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).run {
243 | primaryClip?.let { clipData ->
244 | if (clipData.itemCount > 0) {
245 | clipData.getItemAt(0).text.run {
246 | return runCatching { fromJson(this.toString()) }.getOrNull()?.also {
247 | if (alsoSave) it.save()
248 | }
249 | }
250 | }
251 | }
252 | }
253 | return null
254 | }
255 |
256 | @JvmStatic
257 | fun fromGithub(link: String): CleanData {
258 | val url = URL(
259 | link.replace("www.github.com", "raw.githubusercontent.com")
260 | .replace("github.com", "raw.githubusercontent.com")
261 | )
262 | val conn = url.openConnection() as HttpURLConnection
263 | conn.requestMethod = "GET"
264 | conn.connect()
265 | val inputStream = conn.inputStream
266 | val reader = BufferedReader(InputStreamReader(inputStream))
267 | val sb = StringBuilder()
268 | var line: String? = reader.readLine()
269 | while (line != null) {
270 | sb.append(line)
271 | line = reader.readLine()
272 | }
273 | reader.close()
274 | inputStream.close()
275 | conn.disconnect()
276 | return fromJson(sb.toString())
277 | }
278 |
279 | @JvmStatic
280 | fun createDefaultCleanData(): CleanData {
281 | moduleRes.assets.open(
282 | "${if (HOST_APP.isQqOrTim) "qq" else "wechat"}.json"
283 | ).use {
284 | return fromJson(it.bufferedReader().readText())
285 | }
286 | }
287 | }
288 |
289 | constructor(jsonFile: File) : this(JSONObject(jsonFile.readText())) {
290 | file = jsonFile
291 | }
292 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/dialog/MainConfigDialog.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
2 |
3 | package me.kyuubiran.qqcleanerlite.dialog
4 |
5 | import android.app.Activity
6 | import android.app.AlertDialog
7 | import android.content.Intent
8 | import android.os.Bundle
9 | import android.preference.CheckBoxPreference
10 | import android.preference.Preference
11 | import android.preference.PreferenceCategory
12 | import android.preference.PreferenceFragment
13 | import android.view.View
14 | import android.view.View.OnClickListener
15 | import android.widget.LinearLayout
16 | import android.widget.TextView
17 | import com.github.kyuubiran.ezxhelper.utils.Log
18 | import com.github.kyuubiran.ezxhelper.utils.runOnMainThread
19 | import me.kyuubiran.qqcleanerlite.R
20 | import me.kyuubiran.qqcleanerlite.data.CleanData
21 | import me.kyuubiran.qqcleanerlite.util.CleanManager
22 | import me.kyuubiran.qqcleanerlite.util.Shared
23 | import me.kyuubiran.qqcleanerlite.util.wrapped
24 |
25 | class MainConfigDialog(activity: Activity) : AlertDialog.Builder(activity.wrapped) {
26 |
27 | init {
28 | setTitle("管理配置")
29 |
30 | val fragment = MainConfigFragment()
31 | activity.fragmentManager.beginTransaction().add(fragment, "Config").commit()
32 | activity.fragmentManager.executePendingTransactions()
33 |
34 | fragment.onActivityCreated(null)
35 | setView(fragment.view)
36 |
37 | setPositiveButton("关闭", null)
38 |
39 | show()
40 | }
41 |
42 | class ConfigPreference(
43 | private val activity: Activity,
44 | private val category: PreferenceCategory,
45 | private val cleanData: CleanData
46 | ) : CheckBoxPreference(activity), OnClickListener {
47 |
48 | init {
49 | title = cleanData.title
50 | summary = "作者:${cleanData.author}"
51 | isChecked = cleanData.enable
52 | }
53 |
54 | private fun tv(text: String): TextView = TextView(activity).apply {
55 | setText(text)
56 | setPadding(0, 20, 0, 20)
57 | textSize = 17.0f
58 | layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
59 | }
60 |
61 | private lateinit var toggle: TextView
62 | private lateinit var execute: TextView
63 | private lateinit var edit: TextView
64 | private lateinit var delete: TextView
65 | private lateinit var exportToDownload: TextView
66 | private lateinit var exportToClipboard: TextView
67 |
68 | private val manageDialog by lazy {
69 | toggle = tv(if (cleanData.enable) "禁用" else "启用")
70 | execute = tv("执行")
71 | edit = tv("编辑")
72 | delete = tv("删除")
73 | exportToDownload = tv("导出至下载文件夹")
74 | exportToClipboard = tv("导出至剪贴板")
75 |
76 | AlertDialog.Builder(activity.wrapped).run {
77 | setTitle("操作")
78 |
79 | val ll = LinearLayout(activity).apply {
80 | setPadding(80, 50, 50, 40)
81 | orientation = LinearLayout.VERTICAL
82 |
83 | addView(toggle)
84 | addView(execute)
85 | addView(edit)
86 | addView(delete)
87 | addView(exportToDownload)
88 | addView(exportToClipboard)
89 | }
90 |
91 | setView(ll)
92 | create().apply d@{
93 | toggle.setOnClickListener(this@ConfigPreference)
94 | execute.setOnClickListener(this@ConfigPreference)
95 | edit.setOnClickListener(this@ConfigPreference)
96 | delete.setOnClickListener(this@ConfigPreference)
97 | exportToDownload.setOnClickListener(this@ConfigPreference)
98 | exportToClipboard.setOnClickListener(this@ConfigPreference)
99 | }
100 | }
101 | }
102 |
103 | override fun onClick() {
104 | manageDialog.show()
105 | toggle.text = if (cleanData.enable) "禁用" else "启用"
106 | }
107 |
108 | override fun onClick(p0: View?) {
109 | when (p0) {
110 | toggle -> {
111 | cleanData.enable = !cleanData.enable
112 | cleanData.save()
113 | isChecked = !isChecked
114 | manageDialog.dismiss()
115 | }
116 | execute -> {
117 | cleanData.pushToExecutionQueue(showFinishedToast = true)
118 | manageDialog.dismiss()
119 | }
120 | edit -> {
121 | Shared.currentModify = cleanData
122 | Log.i("Current select : ${cleanData.title}")
123 | ModifyConfigDialog(activity)
124 | manageDialog.dismiss()
125 | }
126 | delete -> {
127 | AlertDialog.Builder(activity.wrapped).run {
128 | setTitle("注意")
129 | setMessage("你真的要删除配置: ${cleanData.title} 吗?")
130 | setPositiveButton("确认") { _, _ ->
131 | cleanData.delete()
132 | category.removePreference(this@ConfigPreference)
133 | Log.toast("删除成功!")
134 | manageDialog.dismiss()
135 | }
136 | setNegativeButton("取消") { _, _ ->
137 | manageDialog.dismiss()
138 | }
139 | show()
140 | }
141 | }
142 | exportToDownload -> {
143 | cleanData.exportToDownload()
144 | Log.toast("导出成功!")
145 | manageDialog.dismiss()
146 | }
147 | exportToClipboard -> {
148 | cleanData.exportToClipboard()
149 | Log.toast("导出成功!")
150 | manageDialog.dismiss()
151 | }
152 | }
153 | }
154 | }
155 |
156 | class MainConfigFragment : PreferenceFragment(),
157 | Preference.OnPreferenceClickListener {
158 |
159 | private lateinit var configList: PreferenceCategory
160 |
161 | override fun onCreate(savedInstanceState: Bundle?) {
162 | super.onCreate(savedInstanceState)
163 | addPreferencesFromResource(R.xml.config_main_prefs)
164 | configList = (findPreference("config_list") as PreferenceCategory).apply cate@{
165 | CleanManager.getAllConfigsAsync {
166 | if (it.isEmpty()) {
167 |
168 | AlertDialog.Builder(activity.wrapped).run {
169 | setTitle("提示")
170 | setMessage("没有可用的配置文件,是否创建默认的配置文件?")
171 | setPositiveButton("是") { _, _ ->
172 | val data = CleanData.createDefaultCleanData()
173 | data.save()
174 | addPreference(ConfigPreference(activity, this@cate, data))
175 | }
176 | setNegativeButton("否", null)
177 | setCancelable(false)
178 |
179 | runOnMainThread { show() }
180 | }
181 | }
182 |
183 | runOnMainThread {
184 | it.forEach { c -> addPreference(ConfigPreference(activity, this@cate, c)) }
185 | }
186 | }
187 | }
188 | // findPreference("from_file").apply {
189 | // onPreferenceClickListener = this@MainConfigFragment
190 | // isEnabled = false
191 | // summary = "暂不支持此操作"
192 | // }
193 | findPreference("from_clipboard").apply {
194 | onPreferenceClickListener = this@MainConfigFragment
195 | }
196 | }
197 |
198 | override fun onPreferenceClick(p0: Preference?): Boolean {
199 | when (p0?.key) {
200 | // "from_file" -> {
201 | // val intent = Intent(Intent.ACTION_GET_CONTENT)
202 | // intent.type = "application/json"
203 | // startActivityForResult(intent, 1)
204 | // }
205 | "from_clipboard" -> {
206 | val data = CleanData.fromClipboard() ?: run {
207 | Log.toast("导入失败!请检查剪切板中的配置是否正确!")
208 | return true
209 | }
210 |
211 | configList.addPreference(ConfigPreference(activity, configList, data))
212 | Log.toast("导入成功!")
213 | }
214 | }
215 | return true
216 | }
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/dialog/ModifyConfigDialog.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
2 |
3 | package me.kyuubiran.qqcleanerlite.dialog
4 |
5 | import android.app.Activity
6 | import android.app.AlertDialog
7 | import android.content.Context
8 | import android.os.Bundle
9 | import android.preference.CheckBoxPreference
10 | import android.preference.PreferenceCategory
11 | import android.preference.PreferenceFragment
12 | import com.github.kyuubiran.ezxhelper.utils.Log
13 | import me.kyuubiran.qqcleanerlite.R
14 | import me.kyuubiran.qqcleanerlite.data.CleanData
15 | import me.kyuubiran.qqcleanerlite.util.Shared
16 | import me.kyuubiran.qqcleanerlite.util.wrapped
17 |
18 | class ModifyConfigDialog(activity: Activity) :
19 | AlertDialog.Builder(activity.wrapped) {
20 |
21 | init {
22 | setTitle("编辑配置")
23 |
24 | val fragment = ModifyConfigFragment()
25 | activity.fragmentManager.beginTransaction().add(fragment, "Modify").commit()
26 | activity.fragmentManager.executePendingTransactions()
27 |
28 | fragment.onActivityCreated(null)
29 | setView(fragment.view)
30 |
31 | setCancelable(false)
32 | setNeutralButton("放弃更改", null)
33 |
34 | setPositiveButton("保存") { _, _ ->
35 | Shared.currentModify.save()
36 | Log.toast("保存成功!")
37 | }
38 |
39 | show()
40 | }
41 |
42 | class PathPreference(
43 | ctx: Context,
44 | private val path: CleanData.PathData
45 | ) : CheckBoxPreference(ctx) {
46 | init {
47 | isChecked = path.enable
48 | title = path.title
49 | }
50 |
51 | override fun onClick() {
52 | super.onClick()
53 | path.enable = isChecked
54 | }
55 | }
56 |
57 | class ModifyConfigFragment : PreferenceFragment() {
58 |
59 | override fun onCreate(savedInstanceState: Bundle?) {
60 | super.onCreate(savedInstanceState)
61 | addPreferencesFromResource(R.xml.config_modify_prefs)
62 |
63 | findPreference("title").apply {
64 | summary = Shared.currentModify.title
65 | }
66 |
67 | findPreference("author").apply {
68 | summary = Shared.currentModify.author
69 | }
70 |
71 | (findPreference("options") as PreferenceCategory).apply {
72 | if (!Shared.currentModify.valid) {
73 | Log.toast("无效的配置文件!")
74 | return
75 | }
76 |
77 | Shared.currentModify.content.forEach {
78 | val preference = PathPreference(activity, it)
79 | addPreference(preference)
80 | }
81 | }
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/dialog/ModuleDialog.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
2 |
3 | package me.kyuubiran.qqcleanerlite.dialog
4 |
5 | import android.app.Activity
6 | import android.app.AlertDialog
7 | import android.os.Bundle
8 | import android.preference.CheckBoxPreference
9 | import android.preference.Preference
10 | import android.preference.PreferenceFragment
11 | import android.text.InputType
12 | import android.widget.EditText
13 | import android.widget.LinearLayout
14 | import com.github.kyuubiran.ezxhelper.utils.Log
15 | import com.github.kyuubiran.ezxhelper.utils.Observe
16 | import com.github.kyuubiran.ezxhelper.utils.addModuleAssetPath
17 | import com.github.kyuubiran.ezxhelper.utils.runOnMainThread
18 | import me.kyuubiran.qqcleanerlite.BuildConfig
19 | import me.kyuubiran.qqcleanerlite.R
20 | import me.kyuubiran.qqcleanerlite.util.*
21 |
22 | class ModuleDialog(activity: Activity) :
23 | AlertDialog.Builder(activity.wrapped) {
24 |
25 | companion object {
26 | val observer by lazy {
27 | Observe(false)
28 | }
29 | }
30 |
31 | init {
32 | activity.addModuleAssetPath()
33 |
34 | val fragment = PrefsFragment()
35 | activity.fragmentManager.beginTransaction().add(fragment, "Setting").commit()
36 | activity.fragmentManager.executePendingTransactions()
37 |
38 | fragment.onActivityCreated(null)
39 |
40 | setView(fragment.view)
41 | setTitle("瘦身模块轻量版")
42 |
43 | setPositiveButton("关闭", null)
44 | setNeutralButton("执行瘦身", null)
45 | setCancelable(false)
46 | show().apply {
47 | getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
48 | AlertDialog.Builder(activity.wrapped).run {
49 | setTitle("注意")
50 | setMessage("确定要执行瘦身吗?")
51 | setPositiveButton("确定") { _, _ ->
52 | ConfigManager.lastCleanTime = System.currentTimeMillis()
53 | CleanManager.executeAll()
54 | observer.value = !observer.value
55 | }
56 | setNegativeButton("取消", null)
57 | show()
58 | }
59 | }
60 | }
61 | }
62 |
63 | class PrefsFragment : PreferenceFragment(), Preference.OnPreferenceChangeListener,
64 | Preference.OnPreferenceClickListener {
65 |
66 | private val onAutoCleanChanged by lazy {
67 | Observe(ConfigManager.enableAutoClean)
68 | }
69 |
70 | override fun onCreate(savedInstanceState: Bundle?) {
71 | super.onCreate(savedInstanceState)
72 | addPreferencesFromResource(R.xml.setting_dialog_prefs)
73 |
74 | (findPreference("enable_auto_clean") as CheckBoxPreference).apply {
75 | isChecked = ConfigManager.enableAutoClean
76 | onPreferenceChangeListener = this@PrefsFragment
77 | }
78 |
79 | findPreference("auto_clean_delay").apply {
80 | isEnabled = ConfigManager.enableAutoClean
81 | summary = if (ConfigManager.enableAutoClean) "${ConfigManager.autoCleanDelay} 小时" else "自动瘦身已关闭"
82 | onPreferenceClickListener = this@PrefsFragment
83 | onAutoCleanChanged.onValueChanged += {
84 | runOnMainThread {
85 | summary = if (it) "${ConfigManager.autoCleanDelay} 小时" else "自动瘦身已关闭"
86 | isEnabled = it
87 | }
88 | }
89 | }
90 |
91 | (findPreference("dont_show_clean_toast") as CheckBoxPreference).apply {
92 | isChecked = ConfigManager.dontShowCleanToast
93 | onPreferenceChangeListener = this@PrefsFragment
94 | }
95 |
96 | findPreference("keep_file_days").apply {
97 | summary = if (ConfigManager.keepFileDays > 0) "保留 ${ConfigManager.keepFileDays} 天以上的文件" else "关闭"
98 | onPreferenceClickListener = this@PrefsFragment
99 | }
100 |
101 | findPreference("module_version").apply {
102 | summary = "${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE})"
103 | }
104 |
105 | findPreference("last_clean_time").apply {
106 | summary = if (ConfigManager.lastCleanTime > 0) getFormatCleanTime() else "还没有执行过清理哦~"
107 | observer.onValueChanged += { runOnMainThread { this.summary = getFormatCleanTime() } }
108 | }
109 |
110 | findPreference("manage_prefs").apply {
111 | onPreferenceClickListener = this@PrefsFragment
112 | }
113 |
114 | findPreference("join_tg_channel").apply {
115 | onPreferenceClickListener = this@PrefsFragment
116 | }
117 |
118 | findPreference("view_project").apply {
119 | onPreferenceClickListener = this@PrefsFragment
120 | }
121 | }
122 |
123 | override fun onPreferenceChange(p0: Preference?, p1: Any?): Boolean {
124 | when (p0?.key) {
125 | "enable_auto_clean" -> {
126 | val b = p1 as Boolean
127 | if (b == ConfigManager.enableAutoClean) return true
128 | ConfigManager.enableAutoClean = b
129 | onAutoCleanChanged.value = b
130 | }
131 | "dont_show_clean_toast" -> ConfigManager.dontShowCleanToast = p1 as Boolean
132 | }
133 | return true
134 | }
135 |
136 | private fun showEditTextDialog(
137 | title: String,
138 | hint: String,
139 | defaultValue: String,
140 | inputType: Int? = null,
141 | onConfirm: (String) -> Unit
142 | ) {
143 | AlertDialog.Builder(activity.wrapped).run {
144 | setTitle(title)
145 |
146 | val et = EditText(activity).apply {
147 | setHint(hint)
148 | setText(defaultValue)
149 | inputType?.let { this.inputType = it }
150 |
151 | val lp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
152 | this.layoutParams = lp
153 | }
154 |
155 | val ll = LinearLayout(activity).apply {
156 | setPadding(20, 20, 20, 20)
157 | addView(et)
158 | }
159 |
160 | setView(ll)
161 |
162 | setPositiveButton("确定") { _, _ ->
163 | onConfirm(et.text.toString())
164 | }
165 | setNegativeButton("取消", null)
166 | show()
167 | }
168 | }
169 |
170 | override fun onPreferenceClick(p0: Preference?): Boolean {
171 | when (p0?.key) {
172 | "auto_clean_delay" -> showEditTextDialog(
173 | "自动清理瘦身",
174 | "请输入自动清理间隔(单位:小时)",
175 | ConfigManager.autoCleanDelay.toString(),
176 | InputType.TYPE_CLASS_NUMBER
177 | ) {
178 | val i = it.toIntOrNull() ?: -1
179 | if (i < 1) {
180 | Log.e("不合法的数字或自动清理间隔不能小于1小时!")
181 | return@showEditTextDialog
182 | }
183 | ConfigManager.autoCleanDelay = i
184 | p0.summary = "$i 小时"
185 | Log.toast("自动清理间隔已更新为 $i 小时")
186 | }
187 | "keep_file_days" -> showEditTextDialog("设置保留天数", "请输入保留天数", ConfigManager.keepFileDays.toString(), InputType.TYPE_CLASS_NUMBER) {
188 | val i = it.toIntOrNull() ?: -1
189 | if (i < 0) {
190 | Log.toast("不合法的数字!")
191 | return@showEditTextDialog
192 | }
193 | p0.summary = if (ConfigManager.keepFileDays > 0) "保留 ${ConfigManager.keepFileDays} 天以上的文件" else "关闭"
194 | ConfigManager.keepFileDays = i
195 | Log.toast("保留天数已更新为 $i 天")
196 | }
197 | "manage_prefs" -> MainConfigDialog(activity)
198 | "join_tg_channel" -> activity.openUrl("https://t.me/QQCleaner")
199 | "view_project" -> activity.openUrl("https://github.com/KyuubiRan/QQCleanerLite")
200 | }
201 | return true
202 | }
203 | }
204 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/hook/AppHook.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.hook
2 |
3 | import android.app.Application
4 | import com.github.kyuubiran.ezxhelper.init.EzXHelperInit
5 | import com.github.kyuubiran.ezxhelper.utils.findMethod
6 | import com.github.kyuubiran.ezxhelper.utils.hookAfter
7 | import me.kyuubiran.qqcleanerlite.util.CleanManager
8 |
9 | object AppHook : BaseHook() {
10 | override fun init() {
11 | findMethod(Application::class.java) { name == "onCreate" }.hookAfter {
12 | EzXHelperInit.initAppContext(it.thisObject as Application, addPath = true, initModuleResources = true)
13 | CleanManager.initAutoClean
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/hook/BaseHook.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.hook
2 |
3 | abstract class BaseHook {
4 | var isInit: Boolean = false
5 | abstract fun init()
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/hook/EntryHook.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.hook
2 |
3 | import android.app.Activity
4 | import android.app.AlertDialog
5 | import android.content.Context
6 | import android.os.Bundle
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.BaseAdapter
10 | import android.widget.ListView
11 | import com.github.kyuubiran.ezxhelper.utils.*
12 | import com.github.kyuubiran.ezxhelper.utils.Log.logeIfThrow
13 | import me.kyuubiran.qqcleanerlite.dialog.ModuleDialog
14 | import me.kyuubiran.qqcleanerlite.util.*
15 | import java.lang.reflect.Method
16 |
17 | object EntryHook : BaseHook() {
18 | private fun showSettingDialog(act: Activity) {
19 | if (ConfigManager.isFirstRun) {
20 | AlertDialog.Builder(act.wrapped).run {
21 | setTitle("注意事项")
22 | setMessage("本插件采用删除文件的方式,以达成减少3A大作App的体积的目的,同时默认的配置文件会清理图片!!!如果你不需要的话记得关掉!本插件开发旨在学习,请勿用于非法用途,否则后果自负。")
23 | setCancelable(false)
24 | setPositiveButton("取消", null)
25 | setNeutralButton("我已知晓") { _, _ ->
26 | ConfigManager.isFirstRun = false
27 | ModuleDialog(act)
28 | }
29 | show()
30 | }
31 | } else {
32 | ModuleDialog(act)
33 | }
34 | }
35 |
36 | private fun initForQqOrTim() {
37 | getMethodByDesc("Lcom/tencent/mobileqq/activity/AboutActivity;->doOnCreate(Landroid/os/Bundle;)Z").hookAfter { param ->
38 | val cFormSimpleItem = loadClassAny(
39 | "com.tencent.mobileqq.widget.FormSimpleItem",
40 | "com.tencent.mobileqq.widget.FormCommonSingleLineItem"
41 | )
42 |
43 | //获取ViewGroup
44 | val vg: ViewGroup = try {
45 | param.thisObject.getObjectAs("a", cFormSimpleItem)
46 | } catch (e: Exception) {
47 | param.thisObject.getObjectOrNullByTypeAs(cFormSimpleItem)!!
48 | }.parent as ViewGroup
49 | //创建入口
50 | val entry = cFormSimpleItem.newInstanceAs(
51 | args(param.thisObject),
52 | argTypes(Context::class.java)
53 | )!!.also {
54 | it.invokeMethod(
55 | "setLeftText",
56 | args("瘦身模块轻量版"),
57 | argTypes(CharSequence::class.java)
58 | )
59 | it.invokeMethod(
60 | "setRightText",
61 | args("芜狐~"),
62 | argTypes(CharSequence::class.java)
63 | )
64 | }
65 | //设置点击事件
66 | entry.setOnClickListener {
67 | showSettingDialog(param.thisObject as Activity)
68 | }
69 | //添加入口
70 | vg.addView(entry, 2)
71 | }
72 | }
73 |
74 | private fun initForWeChat() = runCatching {
75 | val actClass = loadClassAny(
76 | "com.tencent.mm.plugin.setting.ui.setting.SettingsAboutMicroMsgUI",
77 | "com.tencent.mm.ui.setting.SettingsAboutMicroMsgUI"
78 | )
79 | val preferenceClass = loadClass("com.tencent.mm.ui.base.preference.Preference")
80 |
81 | fun getKey(preference: Any): Any = preference.invokeMethod("getKey")
82 | ?: preference.getObject("mKey")
83 |
84 | actClass.getDeclaredMethod("onCreate", Bundle::class.java).hookAfter {
85 | val ctx = it.thisObject
86 | val listView = it.thisObject.invokeMethod("getListView") as? ListView
87 | ?: it.thisObject.getObjectAs("list", ListView::class.java)
88 | val adapter = listView.adapter as BaseAdapter
89 | val addMethod: Method = findMethod(adapter.javaClass) {
90 | returnType == Void.TYPE && parameterTypes.sameAs(preferenceClass, Int::class.java)
91 | }
92 | // 构建一个入口
93 | val entry = loadClass("com.tencent.mm.ui.base.preference.IconPreference")
94 | .getConstructor(Context::class.java)
95 | .newInstance(ctx).apply {
96 | // 设置入口的属性
97 | invokeMethod(
98 | "setKey",
99 | args("QQCleanerLite"),
100 | argTypes(String::class.java)
101 | )
102 | // 新版微信这里坏了
103 | invokeMethod(
104 | "setSummary",
105 | args("芜狐~"),
106 | argTypes(CharSequence::class.java)
107 | )
108 | invokeMethod(
109 | "setTitle",
110 | args("瘦身模块轻量版"),
111 | argTypes(java.lang.CharSequence::class.java)
112 | )
113 | }
114 |
115 | // 在adapter数据变化前添加entry
116 | findMethod(adapter.javaClass) {
117 | name == "notifyDataSetChanged"
118 | }.hookBefore {
119 | if (adapter.count == 0) return@hookBefore
120 | val position = adapter.count - 2
121 | if ("QQCleanerLite" != getKey(adapter.getItem(position))) {
122 | addMethod.invoke(adapter, entry, position)
123 | }
124 | }
125 | }
126 |
127 | // Hook Preference点击事件
128 | findMethod(actClass) {
129 | name == "onPreferenceTreeClick"
130 | && parameterTypes[1].isAssignableFrom(preferenceClass)
131 | }.hookBefore {
132 | if ("QQCleanerLite" == getKey(it.args[1])) {
133 | showSettingDialog(it.thisObject as Activity)
134 | it.result = true
135 | }
136 | }
137 | }.logeIfThrow()
138 |
139 | override fun init() {
140 | when {
141 | HOST_APP.isQqOrTim -> initForQqOrTim()
142 | HOST_APP.isWeChat -> initForWeChat()
143 | }
144 | }
145 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/util/CleanManager.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.util
2 |
3 | import com.github.kyuubiran.ezxhelper.utils.Log
4 | import com.github.kyuubiran.ezxhelper.utils.Log.logeIfThrow
5 | import com.github.kyuubiran.ezxhelper.utils.mainHandler
6 | import me.kyuubiran.qqcleanerlite.data.CleanData
7 | import me.kyuubiran.qqcleanerlite.util.path.CommonPath
8 | import java.io.File
9 | import java.util.concurrent.LinkedBlockingQueue
10 | import java.util.concurrent.ThreadPoolExecutor
11 | import java.util.concurrent.TimeUnit
12 | import kotlin.concurrent.thread
13 |
14 | object CleanManager {
15 | val pool = ThreadPoolExecutor(1, 1, 10L, TimeUnit.MINUTES, LinkedBlockingQueue(256))
16 |
17 | fun execute(data: CleanData, showToast: Boolean = true, forceExec: Boolean = false) {
18 | if (!data.valid) return
19 | if (!data.enable && !forceExec) return
20 | pool.execute e@{
21 | if (showToast) Log.toast("正在执行 ${data.title}")
22 | runCatching {
23 | data.content.forEach f@{ data ->
24 | if (!data.enable) return@f
25 | data.pathList.forEach { path -> deleteAll(PathParser.getFullPath(path)) }
26 | }
27 | }.logeIfThrow("Execute failed, skipped: ${data.title}") {
28 | if (showToast) Log.toast("执行 ${data.title} 时发生错误,已跳过剩余部分")
29 | }
30 | }
31 | }
32 |
33 | fun executeAll(showToast: Boolean = !ConfigManager.dontShowCleanToast) {
34 | if (showToast) Log.toast("开始执行瘦身...")
35 | ConfigManager.lastCleanTime = System.currentTimeMillis()
36 | getAllConfigsAsync {
37 | if (it.isEmpty() || it.all { c -> !c.enable }) {
38 | Log.toast("没有可执行的瘦身配置")
39 | return@getAllConfigsAsync
40 | }
41 |
42 | it.forEach { data -> execute(data, showToast) }
43 | pool.execute { if (showToast) Log.toast("执行完毕") }
44 | }
45 | }
46 |
47 | private fun deleteAll(path: String) {
48 | deleteAll(f = File(path))
49 | }
50 |
51 | private fun deleteAll(
52 | f: File,
53 | keepTime: Long = ConfigManager.keepFileDays.coerceIn(0..365) * 24 * 60 * 60 * 1000L,
54 | ts: Long = System.currentTimeMillis()
55 | ) {
56 | runCatching {
57 | if (!f.exists()) return
58 | if (f.isFile && ts - f.lastModified() > keepTime) f.delete()
59 | else f.listFiles()?.forEach { deleteAll(it, keepTime, ts) } ?: f.delete()
60 | }.logeIfThrow()
61 | }
62 |
63 | fun getConfigDir(): File {
64 | val path = "${CommonPath.publicData.second}/qqcleaner"
65 | val f = File(path)
66 | if (f.exists()) return f
67 | f.mkdirs()
68 | return f
69 | }
70 |
71 | fun getAllConfigsAsync(onFinish: (List) -> Unit) = thread {
72 | onFinish(getAllConfigs())
73 | }
74 |
75 | private fun getAllConfigs(): List {
76 | val arr = ArrayList()
77 | runCatching {
78 | getConfigDir().also { Log.i("Config path:${it.path}") }.listFiles()
79 | ?.forEach { f -> runCatching { arr.add(CleanData(f)) }.logeIfThrow("Failed to load config") }
80 | }.logeIfThrow {
81 | Log.toast("获取瘦身配置时发生了一个错误")
82 | }
83 | return arr
84 | }
85 |
86 | fun isConfigEmpty(): Boolean = getConfigDir().listFiles()?.isEmpty() ?: true
87 |
88 | private object AutoClean : Runnable {
89 | override fun run() {
90 | if (!ConfigManager.enableAutoClean) return
91 | if (System.currentTimeMillis() - ConfigManager.lastCleanTime <= (ConfigManager.autoCleanDelay * 60L * 60L * 1000L)) return
92 | executeAll()
93 | mainHandler.postDelayed(this, 30000L)
94 | }
95 | }
96 |
97 | val initAutoClean = mainHandler.postDelayed(AutoClean, 30000)
98 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/util/ConfigManager.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.util
2 |
3 | import android.content.Context
4 | import com.github.kyuubiran.ezxhelper.init.InitFields
5 |
6 | object ConfigManager {
7 | private val prefs by lazy {
8 | InitFields.appContext.getSharedPreferences("qqcleanerlite", Context.MODE_PRIVATE)
9 | }
10 |
11 | var isFirstRun: Boolean
12 | get() = prefs.getBoolean("isFirstRun", true)
13 | set(value) = prefs.edit().putBoolean("isFirstRun", value).apply()
14 |
15 | var lastCleanTime: Long
16 | get() = prefs.getLong("lastCleanTime", 0)
17 | set(value) = prefs.edit().putLong("lastCleanTime", value).apply()
18 |
19 | var enableAutoClean: Boolean
20 | get() = prefs.getBoolean("enableAutoClean", false)
21 | set(value) = prefs.edit().putBoolean("enableAutoClean", value).apply()
22 |
23 | var autoCleanDelay: Int
24 | get() = prefs.getInt("autoCleanDelay", 24)
25 | set(value) = prefs.edit().putInt("autoCleanDelay", value).apply()
26 |
27 | var keepFileDays: Int
28 | get() = prefs.getInt("keepFileDays", 0)
29 | set(value) = prefs.edit().putInt("keepFileDays", value).apply()
30 |
31 | var dontShowCleanToast: Boolean
32 | get() = prefs.getBoolean("showCleanToast", false)
33 | set(value) = prefs.edit().putBoolean("showCleanToast", value).apply()
34 |
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/util/PathParser.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.util
2 |
3 | import me.kyuubiran.qqcleanerlite.data.CleanData
4 | import me.kyuubiran.qqcleanerlite.util.path.CommonPath
5 | import me.kyuubiran.qqcleanerlite.util.path.QQPath
6 | import me.kyuubiran.qqcleanerlite.util.path.WeChatPath
7 |
8 | object PathParser {
9 | fun getFullPath(path: CleanData.PathData.Path): String {
10 | var tmp = path.suffix
11 |
12 | when (path.prefix) {
13 | CommonPath.publicData.first ->
14 | tmp = CommonPath.publicData.second + path.suffix
15 | CommonPath.privateData.first ->
16 | tmp = CommonPath.privateData.second + path.suffix
17 | QQPath.tencentDir.first ->
18 | if (HOST_APP.isQqOrTim) tmp = QQPath.tencentDir.second + path.suffix
19 | WeChatPath.publicUserData.first ->
20 | if (HOST_APP.isWeChat) tmp = WeChatPath.publicUserData.second + path.suffix
21 | WeChatPath.privateUserData.first ->
22 | if (HOST_APP.isWeChat) tmp = WeChatPath.privateUserData.second + path.suffix
23 | }
24 |
25 | if (tmp == path.suffix) throw IllegalArgumentException("Unsupported path prefix: ${path.prefix}")
26 | return tmp
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/util/Shared.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.util
2 |
3 | import me.kyuubiran.qqcleanerlite.data.CleanData
4 |
5 | object Shared {
6 | lateinit var currentModify: CleanData
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/util/Utils.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.util
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.view.ContextThemeWrapper
7 | import java.text.SimpleDateFormat
8 |
9 | enum class HostAppType {
10 | QQ,
11 | TIM,
12 | WECHAT
13 | }
14 |
15 | lateinit var HOST_APP: HostAppType
16 |
17 | val HostAppType.isQqOrTim: Boolean
18 | get() = this == HostAppType.QQ || this == HostAppType.TIM
19 |
20 | val HostAppType.isWeChat: Boolean
21 | get() = this == HostAppType.WECHAT
22 |
23 | fun HostAppType.validFor(s: String) = when {
24 | this == HostAppType.QQ && s.contains("qq") -> true
25 | this == HostAppType.TIM && s.contains("tim") -> true
26 | this == HostAppType.WECHAT && s.contains("wechat") -> true
27 | else -> false
28 | }
29 |
30 | fun getFormatCleanTime(): String = ConfigManager.lastCleanTime.let { if (it > 0) SimpleDateFormat.getInstance().format(it) else "还没有清理过哦~" }
31 |
32 | val Context.wrapped: Context
33 | get() = ContextThemeWrapper(this, android.R.style.Theme_Material_Dialog_Alert)
34 |
35 | fun Context.openUrl(uriString: String) {
36 | this.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(uriString)))
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/util/path/CommonPath.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.util.path
2 |
3 | import com.github.kyuubiran.ezxhelper.init.InitFields.appContext
4 |
5 | object CommonPath {
6 | //P: storage/emulated/0/Android/data/${HostAppPackageName}
7 | val publicData by lazy {
8 | "!PublicDataDir" to (appContext.externalCacheDir?.parentFile?.path ?: "")
9 | }
10 |
11 | //P: data/user/0/${HostAppPackageName}
12 | val privateData by lazy {
13 | "!PrivateDataDir" to (appContext.filesDir?.parentFile?.path ?: "")
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/util/path/QQPath.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.util.path
2 |
3 | import com.github.kyuubiran.ezxhelper.init.InitFields.appContext
4 |
5 | object QQPath {
6 | //P: storage/emulated/0/tencent
7 | val tencentDir by lazy {
8 | "!TencentDir" to (appContext.obbDir?.parentFile?.parentFile?.parentFile?.path?.let { "${it}/tencent" }
9 | ?: "")
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/qqcleanerlite/util/path/WeChatPath.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.qqcleanerlite.util.path
2 |
3 | import java.io.File
4 |
5 | object WeChatPath {
6 | //P: storage/emulated/0/Android/data/com.tencent.mm/MicroMsg/${UserDataDirName}
7 | val publicUserData: Pair by lazy {
8 | "!PublicUserDataDir" to run {
9 | val dirs = File("${CommonPath.publicData.second}/MicroMsg").listFiles()
10 | if (dirs != null && dirs.isNotEmpty()) {
11 | return@run dirs.firstOrNull { it.name.length == 32 && it.isDirectory }?.absolutePath
12 | ?: ""
13 | }
14 | ""
15 | }
16 | }
17 |
18 | //P: data/user/0/com.tencent.mm/MicroMsg/${UserDataDirName}
19 | val privateUserData: Pair by lazy {
20 | "!PrivateUserDataDir" to run {
21 | val dirs = File("${CommonPath.privateData.second}/MicroMsg").listFiles()
22 | if (dirs != null && dirs.isNotEmpty()) {
23 | return@run dirs.firstOrNull {
24 | it.name.length == 32 && it.isDirectory
25 | && File("${it.absolutePath}/account.bin").exists()
26 | }?.absolutePath ?: ""
27 | }
28 | ""
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/QQCleanerLite/8e19cef4cf46f7d6db20433b797d66838ca06533/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/QQCleanerLite/8e19cef4cf46f7d6db20433b797d66838ca06533/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/QQCleanerLite/8e19cef4cf46f7d6db20433b797d66838ca06533/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/QQCleanerLite/8e19cef4cf46f7d6db20433b797d66838ca06533/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/QQCleanerLite/8e19cef4cf46f7d6db20433b797d66838ca06533/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/QQCleanerLite/8e19cef4cf46f7d6db20433b797d66838ca06533/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/QQCleanerLite/8e19cef4cf46f7d6db20433b797d66838ca06533/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/QQCleanerLite/8e19cef4cf46f7d6db20433b797d66838ca06533/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/QQCleanerLite/8e19cef4cf46f7d6db20433b797d66838ca06533/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/QQCleanerLite/8e19cef4cf46f7d6db20433b797d66838ca06533/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - com.tencent.mobileqq
5 | - com.tencent.tim
6 | - com.tencent.mm
7 |
8 |
--------------------------------------------------------------------------------
/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/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | QQCleaner Lite
3 | 瘦身模块轻量版
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/config_main_prefs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/config_modify_prefs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/setting_dialog_prefs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
12 |
13 |
14 |
15 |
18 |
21 |
24 |
25 |
26 |
27 |
30 |
33 |
37 |
41 |
45 |
46 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application") version "7.2.1" apply false
3 | id("com.android.library") version "7.2.1" apply false
4 | id("org.jetbrains.kotlin.android") version "1.7.10" apply false
5 | }
6 |
7 | tasks.register("clean").configure {
8 | delete(rootProject.buildDir)
9 | }
--------------------------------------------------------------------------------
/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. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec: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 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/QQCleanerLite/8e19cef4cf46f7d6db20433b797d66838ca06533/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Mar 14 19:40:19 CST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven("https://api.xposed.info/")
14 | }
15 | }
16 |
17 | include(":app")
18 | rootProject.name = "QQCleanerLite"
19 |
20 |
--------------------------------------------------------------------------------