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