Skip to content

Commit 6c89b98

Browse files
committed
refactor: replace per-request IPC with in-memory rule snapshot and fix DataManager bugs
1 parent 550a9fe commit 6c89b98

10 files changed

Lines changed: 471 additions & 209 deletions

File tree

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ android {
7373
minSdk = 26
7474
targetSdk = 36
7575
versionCode = calculateVersionCode()
76-
versionName = "4.3.0"
76+
versionName = "4.3.1"
7777

7878
vectorDrawables {
7979
useSupportLibrary = true
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.close.hook.ads.data
2+
3+
import com.close.hook.ads.data.model.RuleMatch
4+
import com.close.hook.ads.data.model.Url
5+
import java.util.Locale
6+
7+
data class RuleSnapshot(
8+
val exactUrls: Set<String>,
9+
val domains: Set<String>,
10+
val keywords: List<String>
11+
) {
12+
fun match(requestValue: String, host: String?): RuleMatch {
13+
val normalizedRequest = requestValue.trim()
14+
val normalizedHost = host?.trim().orEmpty()
15+
16+
if (normalizedRequest.isEmpty()) return RuleMatch.NOT_MATCHED
17+
18+
val lowerRequest = normalizedRequest.lowercase(Locale.ROOT)
19+
val lowerHost = normalizedHost.lowercase(Locale.ROOT)
20+
21+
if (lowerRequest in exactUrls) {
22+
return RuleMatch(matched = true, ruleType = "URL", ruleUrl = normalizedRequest)
23+
}
24+
25+
if (lowerHost.isNotEmpty() && lowerHost in domains) {
26+
return RuleMatch(matched = true, ruleType = "Domain", ruleUrl = normalizedHost)
27+
}
28+
29+
keywords.firstOrNull { keyword ->
30+
keyword.isNotBlank() && lowerRequest.contains(keyword, ignoreCase = true)
31+
}?.let {
32+
return RuleMatch(matched = true, ruleType = "KeyWord", ruleUrl = it)
33+
}
34+
35+
return RuleMatch.NOT_MATCHED
36+
}
37+
38+
companion object {
39+
val EMPTY = RuleSnapshot(
40+
exactUrls = emptySet(),
41+
domains = emptySet(),
42+
keywords = emptyList()
43+
)
44+
45+
fun fromUrls(urls: List<Url>): RuleSnapshot {
46+
if (urls.isEmpty()) return EMPTY
47+
48+
val exactUrls = LinkedHashSet<String>()
49+
val domains = LinkedHashSet<String>()
50+
val keywords = ArrayList<String>()
51+
52+
urls.forEach { rule ->
53+
val type = rule.type.trim()
54+
val value = rule.url.trim()
55+
if (value.isEmpty()) return@forEach
56+
57+
when (type.lowercase(Locale.ROOT)) {
58+
"url" -> exactUrls += value.lowercase(Locale.ROOT)
59+
"domain" -> domains += value.lowercase(Locale.ROOT)
60+
"keyword" -> keywords += value
61+
}
62+
}
63+
64+
return RuleSnapshot(
65+
exactUrls = exactUrls,
66+
domains = domains,
67+
keywords = keywords
68+
)
69+
}
70+
}
71+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.close.hook.ads.data.model
2+
3+
data class RuleMatch(
4+
val matched: Boolean,
5+
val ruleType: String? = null,
6+
val ruleUrl: String? = null
7+
) {
8+
companion object {
9+
val NOT_MATCHED = RuleMatch(false, null, null)
10+
}
11+
}

app/src/main/java/com/close/hook/ads/data/repository/DataManagerRepository.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class DataManagerRepository(private val context: Context) {
3434
val service = ServiceManager.service ?: return@withContext emptyList()
3535
try {
3636
val fileNames = service.listRemoteFiles() ?: return@withContext emptyList()
37-
37+
3838
fileNames.mapNotNull { fileName ->
3939
try {
4040
service.openRemoteFile(fileName).use { pfd ->
@@ -72,22 +72,22 @@ class DataManagerRepository(private val context: Context) {
7272

7373
suspend fun getDatabases(): List<ManagedItem> = withContext(Dispatchers.IO) {
7474
val dbFilter = { name: String ->
75-
!name.endsWith("-journal") &&
76-
!name.endsWith("-shm") &&
75+
!name.endsWith("-journal") &&
76+
!name.endsWith("-shm") &&
7777
!name.endsWith("-wal")
7878
}
7979
getLocalDirectoryItems("databases", null, ItemType.DATABASE, dbFilter)
8080
}
8181

8282
private fun getLocalDirectoryItems(
83-
dirName: String,
84-
extension: String?,
83+
dirName: String,
84+
extension: String?,
8585
itemType: ItemType,
8686
additionalFilter: ((String) -> Boolean)? = null
8787
): List<ManagedItem> {
8888
return try {
8989
val dir = File("${context.applicationInfo.dataDir}/$dirName")
90-
dir.listFiles { _, name ->
90+
dir.listFiles { _, name ->
9191
val extMatch = extension == null || name.endsWith(extension)
9292
val customMatch = additionalFilter?.invoke(name) ?: true
9393
extMatch && customMatch
@@ -172,7 +172,6 @@ class DataManagerRepository(private val context: Context) {
172172
val file = File("${context.applicationInfo.dataDir}/shared_prefs", "$groupName.xml")
173173
if (file.exists()) file.delete() else false
174174
}
175-
true
176175
} catch (e: Exception) {
177176
Log.e("DataManagerRepository", "Failed to delete local preference group: $groupName", e)
178177
false
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package com.close.hook.ads.data.repository
2+
3+
import android.content.Context
4+
import android.database.ContentObserver
5+
import android.net.Uri
6+
import android.os.Handler
7+
import android.os.Looper
8+
import android.util.Log
9+
import com.close.hook.ads.data.model.RuleMatch
10+
import com.close.hook.ads.data.model.Url
11+
import com.close.hook.ads.data.RuleSnapshot
12+
import com.close.hook.ads.provider.UrlContentProvider
13+
import java.util.concurrent.atomic.AtomicBoolean
14+
15+
object RuleRepository {
16+
17+
private const val LOG_PREFIX = "[RuleRepository]"
18+
private const val MIN_REFRESH_INTERVAL_MS = 5_000L
19+
20+
private val contentUri: Uri = Uri.Builder()
21+
.scheme("content")
22+
.authority(UrlContentProvider.AUTHORITY)
23+
.appendPath(UrlContentProvider.URL_TABLE_NAME)
24+
.build()
25+
26+
@Volatile private var appContext: Context? = null
27+
@Volatile private var snapshot: RuleSnapshot = RuleSnapshot.EMPTY
28+
@Volatile private var lastRefreshAt: Long = 0L
29+
30+
private val initialized = AtomicBoolean(false)
31+
private val dirty = AtomicBoolean(true)
32+
private val refreshLock = Any()
33+
34+
private val observer by lazy {
35+
object : ContentObserver(
36+
null
37+
) {
38+
override fun onChange(selfChange: Boolean) {
39+
dirty.set(true)
40+
}
41+
42+
override fun onChange(selfChange: Boolean, uri: Uri?) {
43+
dirty.set(true)
44+
}
45+
}
46+
}
47+
48+
fun init(context: Context) {
49+
appContext = context
50+
}
51+
52+
fun shouldBlock(requestValue: String, host: String?): RuleMatch {
53+
ensureFreshSnapshot(force = false)
54+
return snapshot.match(requestValue = requestValue, host = host)
55+
}
56+
57+
private fun ensureFreshSnapshot(force: Boolean) {
58+
val rawContext = appContext ?: return
59+
60+
val now = System.currentTimeMillis()
61+
val shouldRefresh = force || dirty.get() || (now - lastRefreshAt >= MIN_REFRESH_INTERVAL_MS)
62+
if (!shouldRefresh) return
63+
64+
synchronized(refreshLock) {
65+
val freshNow = System.currentTimeMillis()
66+
val stillNeedRefresh = force || dirty.get() || (freshNow - lastRefreshAt >= MIN_REFRESH_INTERVAL_MS)
67+
if (!stillNeedRefresh) return
68+
69+
if (initialized.compareAndSet(false, true)) {
70+
try {
71+
val safeContext = rawContext.applicationContext ?: rawContext
72+
safeContext.contentResolver.registerContentObserver(contentUri, true, observer)
73+
} catch (e: Throwable) {
74+
Log.w(LOG_PREFIX, "Failed to register observer: ${e.message}")
75+
initialized.set(false)
76+
}
77+
}
78+
79+
runCatching {
80+
val safeContext = rawContext.applicationContext ?: rawContext
81+
val rules = loadAllRules(safeContext)
82+
snapshot = RuleSnapshot.fromUrls(rules)
83+
dirty.set(false)
84+
}.onFailure { error ->
85+
Log.w(LOG_PREFIX, "Failed to refresh rule snapshot: ${error.message}")
86+
}
87+
lastRefreshAt = freshNow
88+
}
89+
}
90+
91+
private fun loadAllRules(context: Context): List<Url> {
92+
val result = ArrayList<Url>()
93+
context.contentResolver.query(
94+
contentUri,
95+
arrayOf(Url.URL_TYPE, Url.URL_ADDRESS),
96+
null, null, null
97+
)?.use { cursor ->
98+
val typeIndex = cursor.getColumnIndex(Url.URL_TYPE)
99+
val urlIndex = cursor.getColumnIndex(Url.URL_ADDRESS)
100+
if (typeIndex == -1 || urlIndex == -1) return emptyList()
101+
while (cursor.moveToNext()) {
102+
val type = cursor.getString(typeIndex).orEmpty()
103+
val url = cursor.getString(urlIndex).orEmpty()
104+
result += Url(type = type, url = url)
105+
}
106+
}
107+
return result
108+
}
109+
}

0 commit comments

Comments
 (0)