11package io.github.cpatcher.handlers
22
33import android.app.Application
4- import android.content.Context
4+ import android.app.ActivityThread // Hidden API (Rikka Stub)
5+ import android.os.Process
6+ import android.os.UserHandle // Hidden API (Rikka Stub)
57import io.github.cpatcher.arch.IHook
6- import io.github.cpatcher.arch.hookAllAfter
7- import io.github.cpatcher.logE
8+ import io.github.cpatcher.arch.hookAfter
89import io.github.cpatcher.logI
10+ import io.github.cpatcher.logE
911import io.github.cpatcher.logW
1012import java.io.File
1113import java.util.concurrent.Executors
12- import java.util.concurrent.TimeUnit
14+ import java.util.concurrent.ThreadFactory
1315
1416/* *
15- * CleanHandler - Automatic cache cleanup module for LSPosed-scoped applications
16- *
17- * Technical approach:
18- * - Hooks into Application.onCreate() for earliest possible cleanup
19- * - Performs asynchronous deletion to avoid blocking UI thread
20- * - Implements comprehensive error containment per directory
21- * - Tracks cleanup metrics for performance monitoring
17+ * CleanHandler (Hybrid Version)
18+ * * Kết hợp sức mạnh của:
19+ * 1. HookUtils DSL: Để can thiệp vào vòng đời Application.
20+ * 2. Hidden APIs: Để truy xuất thông tin hệ thống (Process, User) không qua Reflection.
2221 */
2322class CleanHandler : IHook () {
23+
2424 companion object {
25- private const val CLEANUP_THREAD_NAME = " CpatcherCacheCleanup"
26- private const val CLEANUP_TIMEOUT_MS = 5000L
25+ private const val THREAD_NAME = " Cpatcher-Cleaner"
26+
27+ // Tối ưu hóa Thread: Priority thấp để không ảnh hưởng khởi động App
2728 private val cleanupExecutor = Executors .newSingleThreadExecutor { r ->
28- Thread (r, CLEANUP_THREAD_NAME ).apply {
29+ Thread (r, THREAD_NAME ).apply {
2930 priority = Thread .MIN_PRIORITY
3031 isDaemon = true
3132 }
3233 }
3334 }
34-
35+
3536 override fun onHook () {
36- // MANDATORY: Package validation - only process if we're in a targeted app
37- // Since this is a universal handler, we check if it's NOT our own module
38- if (loadPackageParam.packageName == " io.github.cpatcher" ) {
39- logI(" ${this ::class .simpleName} : Skipping self-cleanup" )
40- return
41- }
42-
43- // Hook Application onCreate for earliest cleanup opportunity
44- Application ::class .java.hookAllAfter(" onCreate" ) { param ->
45- val application = param.thisObject as ? Application
46- if (application == null ) {
47- logW(" ${this ::class .simpleName} : Unable to cast to Application" )
48- return @hookAllAfter
49- }
50-
37+ // RULE 3: Isolation - Logic nằm gọn trong IHook
38+ if (loadPackageParam.packageName == " io.github.cpatcher" ) return
39+
40+ // RULE 2: HookUtils DSL Standard
41+ // Thay vì XposedHelpers.findAndHookMethod, ta dùng extension function
42+ Application ::class .java.hookAfter(" onCreate" ) { param ->
43+ val app = param.thisObject as ? Application ? : return @hookAfter
44+
5145 runCatching {
52- performCacheCleanup(application)
53- }.onFailure { t ->
54- logE(" ${this ::class .simpleName} : Failed to initiate cache cleanup" , t)
46+ // RULE 1: Hybrid Interaction Mandate
47+ // Gọi trực tiếp Hidden API, KHÔNG dùng Reflection.
48+ // ActivityThread.currentProcessName() và UserHandle.myUserId()
49+ // được cung cấp bởi thư viện dev.rikka.hidden.
50+ val currentProcess = ActivityThread .currentProcessName()
51+ val userId = UserHandle .myUserId()
52+
53+ logI(" Initiating cache purge for $currentProcess (UID: ${Process .myUid()} , User: $userId )" )
54+
55+ performHybridCleanup(app)
56+ }.onFailure {
57+ logE(" Failed to init cleanup hook" , it)
5558 }
5659 }
57-
58- logI(" ${this ::class .simpleName} : Successfully initialized for ${loadPackageParam.packageName} " )
5960 }
60-
61- private fun performCacheCleanup (context : Context ) {
61+
62+ private fun performHybridCleanup (context : Application ) {
6263 cleanupExecutor.execute {
6364 runCatching {
64- val startTime = System .currentTimeMillis()
65- var totalDeleted = 0L
66- var filesDeleted = 0
67-
68- // External cache cleanup - typically largest
69- context.externalCacheDir?.let { dir ->
70- if (dir.exists()) {
71- val result = deleteDirectoryContents(dir)
72- totalDeleted + = result.first
73- filesDeleted + = result.second
74- logI(" ${this ::class .simpleName} : External cache - " +
75- " ${result.second} files, ${result.first / 1024 } KB" )
76- }
77- }
78-
79- // Internal cache cleanup
80- context.cacheDir?.let { dir ->
81- if (dir.exists()) {
82- val result = deleteDirectoryContents(dir)
83- totalDeleted + = result.first
84- filesDeleted + = result.second
85- logI(" ${this ::class .simpleName} : Internal cache - " +
86- " ${result.second} files, ${result.first / 1024 } KB" )
87- }
88- }
89-
90- // Code cache cleanup - JIT compiled code
91- context.codeCacheDir?.let { dir ->
65+ val start = System .currentTimeMillis()
66+ var freedBytes = 0L
67+ var fileCount = 0
68+
69+ // Danh sách các thư mục cache tiêu chuẩn
70+ val cacheDirs = listOfNotNull(
71+ context.externalCacheDir,
72+ context.cacheDir,
73+ context.codeCacheDir
74+ )
75+
76+ cacheDirs.forEach { dir ->
9277 if (dir.exists()) {
93- val result = deleteDirectoryContents(dir)
94- totalDeleted + = result.first
95- filesDeleted + = result.second
96- logI(" ${this ::class .simpleName} : Code cache - " +
97- " ${result.second} files, ${result.first / 1024 } KB" )
78+ val (bytes, count) = deleteRecursively(dir)
79+ freedBytes + = bytes
80+ fileCount + = count
9881 }
9982 }
100-
101- val elapsed = System .currentTimeMillis() - startTime
102-
103- if (filesDeleted > 0 ) {
104- logI(" ${this ::class .simpleName} : Cleanup completed - " +
105- " $filesDeleted files, ${totalDeleted / 1024 } KB in ${elapsed} ms" )
106- } else {
107- logI(" ${this ::class .simpleName} : No cache files to clean" )
83+
84+ if (fileCount > 0 ) {
85+ val time = System .currentTimeMillis() - start
86+ logI(" Cleanup complete: Removed $fileCount files (${freedBytes / 1024 } KB) in ${time} ms" )
10887 }
109-
110- }.onFailure { t ->
111- logE(" ${this ::class .simpleName} : Cache cleanup thread failed" , t)
112- }
113- }
114- }
115-
116- /* *
117- * Recursively deletes directory contents while preserving the root directory
118- * @return Pair of (total bytes deleted, file count)
119- */
120- private fun deleteDirectoryContents (directory : File ): Pair <Long , Int > {
121- var totalSize = 0L
122- var fileCount = 0
123-
124- runCatching {
125- directory.listFiles()?.forEach { file ->
126- val result = deleteRecursively(file)
127- totalSize + = result.first
128- fileCount + = result.second
88+ }.onFailure {
89+ logE(" Cleanup thread error" , it)
12990 }
130- }.onFailure { t ->
131- logE(" ${this ::class .simpleName} : Failed to list ${directory.absolutePath} " , t)
13291 }
133-
134- return Pair (totalSize, fileCount)
13592 }
136-
137- /* *
138- * Recursively deletes a file or directory
139- * @return Pair of (total bytes deleted, file count)
140- */
93+
94+ // Helper: Đệ quy xóa file an toàn, trả về (Bytes, Số lượng)
14195 private fun deleteRecursively (file : File ): Pair <Long , Int > {
142- if (! file.exists()) return Pair (0L , 0 )
143-
144- var totalSize = 0L
145- var fileCount = 0
96+ var size = 0L
97+ var count = 0
14698
147- runCatching {
148- if (file.isDirectory) {
149- file.listFiles()?.forEach { child ->
150- val result = deleteRecursively( child)
151- totalSize + = result.first
152- fileCount + = result.second
153- }
99+ if ( ! file.exists()) return 0L to 0
100+
101+ if ( file.isDirectory) {
102+ file.listFiles()?.forEach { child ->
103+ val (childSize, childCount) = deleteRecursively(child)
104+ size + = childSize
105+ count + = childCount
154106 }
155-
156- val fileSize = file.length()
107+ }
108+
109+ // Không xóa thư mục gốc (cache, code_cache) để tránh lỗi permission cục bộ
110+ // Chỉ xóa nội dung bên trong hoặc thư mục con
111+ val isRootCache = file.name == " cache" || file.name == " code_cache"
112+ if (! isRootCache) {
113+ val length = file.length()
157114 if (file.delete()) {
158- totalSize + = fileSize
159- fileCount++
160- }
161-
162- }.onFailure { t ->
163- // Silent failure for individual files - don't spam logs
164- if (file.isDirectory) {
165- logW(" ${this ::class .simpleName} : Failed to delete directory ${file.name} " )
115+ size + = length
116+ count++
166117 }
167118 }
168119
169- return Pair (totalSize, fileCount)
170- }
171-
172- /* *
173- * Cleanup executor on module unload (if supported by framework)
174- */
175- fun cleanup () {
176- runCatching {
177- cleanupExecutor.shutdown()
178- if (! cleanupExecutor.awaitTermination(CLEANUP_TIMEOUT_MS , TimeUnit .MILLISECONDS )) {
179- cleanupExecutor.shutdownNow()
180- }
181- }.onFailure { t ->
182- logE(" ${this ::class .simpleName} : Failed to shutdown executor" , t)
183- }
120+ return size to count
184121 }
185- }
122+ }
0 commit comments