diff --git a/app/src/main/kotlin/li/songe/gkd/MainActivity.kt b/app/src/main/kotlin/li/songe/gkd/MainActivity.kt index 2be2f98..1cf70dc 100644 --- a/app/src/main/kotlin/li/songe/gkd/MainActivity.kt +++ b/app/src/main/kotlin/li/songe/gkd/MainActivity.kt @@ -208,11 +208,11 @@ private fun Activity.fixTopPadding() { @Composable private fun ShizukuErrorDialog(stateFlow: MutableStateFlow) { - val state = stateFlow.collectAsState() - if (state.value) { + val state = stateFlow.collectAsState().value + if (state) { val appId = "moe.shizuku.privileged.api" - val appInfoCache = appInfoCacheFlow.collectAsState() - val installed = appInfoCache.value.contains(appId) + val appInfoCache = appInfoCacheFlow.collectAsState().value + val installed = appInfoCache.contains(appId) AlertDialog( onDismissRequest = { stateFlow.value = false }, title = { Text(text = "授权错误") }, diff --git a/app/src/main/kotlin/li/songe/gkd/data/AppRule.kt b/app/src/main/kotlin/li/songe/gkd/data/AppRule.kt index a3e475e..32b2c83 100644 --- a/app/src/main/kotlin/li/songe/gkd/data/AppRule.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/AppRule.kt @@ -1,7 +1,5 @@ package li.songe.gkd.data -import li.songe.gkd.util.ResolvedAppGroup - class AppRule( rule: RawSubscription.RawAppRule, g: ResolvedAppGroup, diff --git a/app/src/main/kotlin/li/songe/gkd/data/ComplexSnapshot.kt b/app/src/main/kotlin/li/songe/gkd/data/ComplexSnapshot.kt index 798b4a4..b7c17c3 100644 --- a/app/src/main/kotlin/li/songe/gkd/data/ComplexSnapshot.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/ComplexSnapshot.kt @@ -48,7 +48,7 @@ fun createComplexSnapshot(): ComplexSnapshot { screenWidth = ScreenUtils.getScreenWidth(), isLandscape = ScreenUtils.isLandscape(), - nodes = NodeInfo.info2nodeList(currentAbNode) + nodes = info2nodeList(currentAbNode) ) } diff --git a/app/src/main/kotlin/li/songe/gkd/data/GlobalRule.kt b/app/src/main/kotlin/li/songe/gkd/data/GlobalRule.kt index d25d5c8..d58927d 100644 --- a/app/src/main/kotlin/li/songe/gkd/data/GlobalRule.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/GlobalRule.kt @@ -1,7 +1,6 @@ package li.songe.gkd.data import li.songe.gkd.service.launcherAppId -import li.songe.gkd.util.ResolvedGlobalGroup import li.songe.gkd.util.systemAppsFlow data class GlobalApp( diff --git a/app/src/main/kotlin/li/songe/gkd/data/NodeInfo.kt b/app/src/main/kotlin/li/songe/gkd/data/NodeInfo.kt index fe2397b..a8f98c9 100644 --- a/app/src/main/kotlin/li/songe/gkd/data/NodeInfo.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/NodeInfo.kt @@ -3,7 +3,7 @@ package li.songe.gkd.data import android.view.accessibility.AccessibilityNodeInfo import com.blankj.utilcode.util.LogUtils import kotlinx.serialization.Serializable -import li.songe.gkd.service.getChildren +import li.songe.gkd.service.MAX_CHILD_SIZE import li.songe.gkd.service.topActivityFlow import li.songe.gkd.util.toast import kotlin.system.measureTimeMillis @@ -15,203 +15,206 @@ data class NodeInfo( val idQf: Boolean?, val textQf: Boolean?, val attr: AttrInfo, +) + +private data class TempNodeData( + val node: AccessibilityNodeInfo, + val parent: TempNodeData?, + val index: Int, + val depth: Int, ) { + var id = 0 + val attr = AttrInfo.info2data(node, index, depth) + var children: List = emptyList() - companion object { - - private const val MAX_KEEP_SIZE = 5000 - - // 先获取所有节点构建树结构, 然后再判断 idQf/textQf 如果存在一个能同时 idQf 和 textQf 的节点, 则认为 idQf 和 textQf 等价 - - data class TempNodeData( - val node: AccessibilityNodeInfo, - val parent: TempNodeData?, - val index: Int, - val depth: Int, - ) { - var id = 0 - val attr = AttrInfo.info2data(node, index, depth) - var children: List = emptyList() - - var idQfInit = false - var idQf: Boolean? = null - set(value) { - field = value - idQfInit = true - } - var textQfInit = false - var textQf: Boolean? = null - set(value) { - field = value - textQfInit = true - } + var idQfInit = false + var idQf: Boolean? = null + set(value) { + field = value + idQfInit = true } + var textQfInit = false + var textQf: Boolean? = null + set(value) { + field = value + textQfInit = true + } +} - fun info2nodeList(root: AccessibilityNodeInfo?): List { - if (root == null) { - return emptyList() - } - val nodes = mutableListOf() - val collectTime = measureTimeMillis { - val stack = mutableListOf() - var times = 0 - stack.add(TempNodeData(root, null, 0, 0)) - while (stack.isNotEmpty()) { - times++ - val node = stack.removeAt(stack.lastIndex) - node.id = times - 1 - val children = getChildren(node.node).mapIndexed { i, child -> - TempNodeData( - child, node, i, node.depth + 1 - ) - }.toList() - node.children = children - nodes.add(node) - repeat(children.size) { i -> - stack.add(children[children.size - i - 1]) - } - if (times > MAX_KEEP_SIZE) { - // https://github.com/gkd-kit/gkd/issues/28 - toast("节点数量至多保留$MAX_KEEP_SIZE,丢弃后续节点") - LogUtils.w( - root.packageName, topActivityFlow.value.activityId, "节点数量过多" - ) - break - } - } - } - val qfTime = measureTimeMillis { - val idQfCache = mutableMapOf>() - val textQfCache = mutableMapOf>() - var idTextQf = false - fun updateQf(n: TempNodeData) { - if (!n.idQfInit && !n.attr.id.isNullOrEmpty()) { - n.idQf = (idQfCache[n.attr.id] - ?: root.findAccessibilityNodeInfosByViewId(n.attr.id)).apply { - idQfCache[n.attr.id] = this - } - .any { t -> t == n.node } +private fun getChildren(node: AccessibilityNodeInfo) = sequence { + repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { i -> + val child = node.getChild(i) ?: return@sequence + yield(child) + } +} - } +private const val MAX_KEEP_SIZE = 5000 - if (!n.textQfInit && !n.attr.text.isNullOrEmpty()) { - n.textQf = (textQfCache[n.attr.text] - ?: root.findAccessibilityNodeInfosByText(n.attr.text)).apply { - textQfCache[n.attr.text] = this - } - .any { t -> t == n.node } - } - - if (n.idQf == true && n.textQf == true) { - idTextQf = true - } - - if (!n.idQfInit && n.idQf != null) { - n.parent?.children?.forEach { c -> - c.idQf = n.idQf - if (idTextQf) { - c.textQf = n.textQf - } - } - if (n.idQf == true) { - var p = n.parent - while (p != null && !p.idQfInit) { - p.idQf = n.idQf - if (idTextQf) { - p.textQf = n.textQf - } - p = p.parent - p?.children?.forEach { bro -> - bro.idQf = n.idQf - if (idTextQf) { - bro.textQf = n.textQf - } - } - } - } else { - val tempStack = mutableListOf(n) - while (tempStack.isNotEmpty()) { - val top = tempStack.removeAt(tempStack.lastIndex) - top.idQf = n.idQf - if (idTextQf) { - top.textQf = n.textQf - } - repeat(top.children.size) { i -> - tempStack.add(top.children[top.children.size - i - 1]) - } - } - } - } - - if (!n.textQfInit && n.textQf != null) { - n.parent?.children?.forEach { c -> - c.textQf = n.textQf - if (idTextQf) { - c.idQf = n.idQf - } - } - if (n.textQf == true) { - var p = n.parent - while (p != null && !p.textQfInit) { - p.textQf = n.textQf - if (idTextQf) { - p.idQf = n.idQf - } - p = p.parent - p?.children?.forEach { bro -> - bro.textQf = n.textQf - if (idTextQf) { - bro.idQf = bro.idQf - } - } - } - } else { - val tempStack = mutableListOf(n) - while (tempStack.isNotEmpty()) { - val top = tempStack.removeAt(tempStack.lastIndex) - top.textQf = n.textQf - if (idTextQf) { - top.idQf = n.idQf - } - repeat(top.children.size) { i -> - tempStack.add(top.children[top.children.size - i - 1]) - } - } - } - } - - n.idQfInit = true - n.textQfInit = true - } - for (i in (nodes.size - 1) downTo 0) { - val n = nodes[i] - if (n.children.isEmpty()) { - updateQf(n) - } - } - for (i in (nodes.size - 1) downTo 0) { - val n = nodes[i] - if (n.children.isNotEmpty()) { - updateQf(n) - } - } - } - - LogUtils.d( - topActivityFlow.value, - "快照节点数量:${nodes.size}, 总耗时:${collectTime + qfTime}ms", - "收集节点耗时:${collectTime}ms, 收集quickFind耗时:${qfTime}ms", - ) - - return nodes.map { n -> - NodeInfo( - id = n.id, - pid = n.parent?.id ?: -1, - idQf = n.idQf, - textQf = n.textQf, - attr = n.attr +// 先获取所有节点构建树结构, 然后再判断 idQf/textQf 如果存在一个能同时 idQf 和 textQf 的节点, 则认为 idQf 和 textQf 等价 +fun info2nodeList(root: AccessibilityNodeInfo?): List { + if (root == null) { + return emptyList() + } + val nodes = mutableListOf() + val collectTime = measureTimeMillis { + val stack = mutableListOf() + var times = 0 + stack.add(TempNodeData(root, null, 0, 0)) + while (stack.isNotEmpty()) { + times++ + val node = stack.removeAt(stack.lastIndex) + node.id = times - 1 + val children = getChildren(node.node).mapIndexed { i, child -> + TempNodeData( + child, node, i, node.depth + 1 ) + }.toList() + node.children = children + nodes.add(node) + repeat(children.size) { i -> + stack.add(children[children.size - i - 1]) + } + if (times > MAX_KEEP_SIZE) { + // https://github.com/gkd-kit/gkd/issues/28 + toast("节点数量至多保留$MAX_KEEP_SIZE,丢弃后续节点") + LogUtils.w( + root.packageName, topActivityFlow.value.activityId, "节点数量过多" + ) + break } } } + val qfTime = measureTimeMillis { + val idQfCache = mutableMapOf>() + val textQfCache = mutableMapOf>() + var idTextQf = false + fun updateQf(n: TempNodeData) { + if (!n.idQfInit && !n.attr.id.isNullOrEmpty()) { + n.idQf = (idQfCache[n.attr.id] + ?: root.findAccessibilityNodeInfosByViewId(n.attr.id)).apply { + idQfCache[n.attr.id] = this + } + .any { t -> t == n.node } + + } + + if (!n.textQfInit && !n.attr.text.isNullOrEmpty()) { + n.textQf = (textQfCache[n.attr.text] + ?: root.findAccessibilityNodeInfosByText(n.attr.text)).apply { + textQfCache[n.attr.text] = this + } + .any { t -> t == n.node } + } + + if (n.idQf == true && n.textQf == true) { + idTextQf = true + } + + if (!n.idQfInit && n.idQf != null) { + n.parent?.children?.forEach { c -> + c.idQf = n.idQf + if (idTextQf) { + c.textQf = n.textQf + } + } + if (n.idQf == true) { + var p = n.parent + while (p != null && !p.idQfInit) { + p.idQf = n.idQf + if (idTextQf) { + p.textQf = n.textQf + } + p = p.parent + p?.children?.forEach { bro -> + bro.idQf = n.idQf + if (idTextQf) { + bro.textQf = n.textQf + } + } + } + } else { + val tempStack = mutableListOf(n) + while (tempStack.isNotEmpty()) { + val top = tempStack.removeAt(tempStack.lastIndex) + top.idQf = n.idQf + if (idTextQf) { + top.textQf = n.textQf + } + repeat(top.children.size) { i -> + tempStack.add(top.children[top.children.size - i - 1]) + } + } + } + } + + if (!n.textQfInit && n.textQf != null) { + n.parent?.children?.forEach { c -> + c.textQf = n.textQf + if (idTextQf) { + c.idQf = n.idQf + } + } + if (n.textQf == true) { + var p = n.parent + while (p != null && !p.textQfInit) { + p.textQf = n.textQf + if (idTextQf) { + p.idQf = n.idQf + } + p = p.parent + p?.children?.forEach { bro -> + bro.textQf = n.textQf + if (idTextQf) { + bro.idQf = bro.idQf + } + } + } + } else { + val tempStack = mutableListOf(n) + while (tempStack.isNotEmpty()) { + val top = tempStack.removeAt(tempStack.lastIndex) + top.textQf = n.textQf + if (idTextQf) { + top.idQf = n.idQf + } + repeat(top.children.size) { i -> + tempStack.add(top.children[top.children.size - i - 1]) + } + } + } + } + + n.idQfInit = true + n.textQfInit = true + } + for (i in (nodes.size - 1) downTo 0) { + val n = nodes[i] + if (n.children.isEmpty()) { + updateQf(n) + } + } + for (i in (nodes.size - 1) downTo 0) { + val n = nodes[i] + if (n.children.isNotEmpty()) { + updateQf(n) + } + } + } + + LogUtils.d( + topActivityFlow.value, + "快照节点数量:${nodes.size}, 总耗时:${collectTime + qfTime}ms", + "收集节点耗时:${collectTime}ms, 收集quickFind耗时:${qfTime}ms", + ) + + return nodes.map { n -> + NodeInfo( + id = n.id, + pid = n.parent?.id ?: -1, + idQf = n.idQf, + textQf = n.textQf, + attr = n.attr + ) + } } + diff --git a/app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt b/app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt index 33a0938..2471a50 100644 --- a/app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt @@ -190,6 +190,8 @@ data class RawSubscription( val snapshotUrls: List? val excludeSnapshotUrls: List? val exampleUrls: List? + val priorityTime: Long? + val priorityActionMaximum: Int? } sealed interface RawRuleProps : RawCommonProps { @@ -267,6 +269,8 @@ data class RawSubscription( override val resetMatch: String?, override val actionCdKey: Int?, override val actionMaximumKey: Int?, + override val priorityTime: Long?, + override val priorityActionMaximum: Int?, override val order: Int?, override val forcedTime: Long?, override val snapshotUrls: List?, @@ -306,6 +310,8 @@ data class RawSubscription( override val resetMatch: String?, override val actionCdKey: Int?, override val actionMaximumKey: Int?, + override val priorityTime: Long?, + override val priorityActionMaximum: Int?, override val order: Int?, override val forcedTime: Long?, override val snapshotUrls: List?, @@ -340,6 +346,8 @@ data class RawSubscription( override val fastQuery: Boolean?, override val matchRoot: Boolean?, override val actionMaximum: Int?, + override val priorityTime: Long?, + override val priorityActionMaximum: Int?, override val order: Int?, override val forcedTime: Long?, override val matchDelay: Long?, @@ -385,6 +393,8 @@ data class RawSubscription( override val fastQuery: Boolean?, override val matchRoot: Boolean?, override val actionMaximum: Int?, + override val priorityTime: Long?, + override val priorityActionMaximum: Int?, override val order: Int?, override val forcedTime: Long?, override val matchDelay: Long?, @@ -626,6 +636,8 @@ data class RawSubscription( excludeVersionNames = getStringIArray(jsonObject, "excludeVersionNames"), position = getPosition(jsonObject), forcedTime = getLong(jsonObject, "forcedTime"), + priorityTime = getLong(jsonObject, "priorityTime"), + priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"), ) } @@ -671,6 +683,8 @@ data class RawSubscription( excludeVersionCodes = getLongIArray(jsonObject, "excludeVersionCodes"), versionNames = getStringIArray(jsonObject, "versionNames"), excludeVersionNames = getStringIArray(jsonObject, "excludeVersionNames"), + priorityTime = getLong(jsonObject, "priorityTime"), + priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"), ) } @@ -736,6 +750,8 @@ data class RawSubscription( order = getInt(jsonObject, "order"), forcedTime = getLong(jsonObject, "forcedTime"), position = getPosition(jsonObject), + priorityTime = getLong(jsonObject, "priorityTime"), + priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"), ) } @@ -773,6 +789,8 @@ data class RawSubscription( order = getInt(jsonObject, "order"), scopeKeys = getIntIArray(jsonObject, "scopeKeys"), forcedTime = getLong(jsonObject, "forcedTime"), + priorityTime = getLong(jsonObject, "priorityTime"), + priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"), ) } diff --git a/app/src/main/kotlin/li/songe/gkd/util/ResolvedGroup.kt b/app/src/main/kotlin/li/songe/gkd/data/ResolvedGroup.kt similarity index 82% rename from app/src/main/kotlin/li/songe/gkd/util/ResolvedGroup.kt rename to app/src/main/kotlin/li/songe/gkd/data/ResolvedGroup.kt index a623b24..5d96950 100644 --- a/app/src/main/kotlin/li/songe/gkd/util/ResolvedGroup.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/ResolvedGroup.kt @@ -1,15 +1,13 @@ -package li.songe.gkd.util - -import li.songe.gkd.data.RawSubscription -import li.songe.gkd.data.SubsConfig -import li.songe.gkd.data.SubsItem +package li.songe.gkd.data sealed class ResolvedGroup( open val group: RawSubscription.RawGroupProps, val subscription: RawSubscription, val subsItem: SubsItem, val config: SubsConfig?, -) +) { + val excludeData = ExcludeData.parse(config?.exclude) +} class ResolvedAppGroup( override val group: RawSubscription.RawAppGroup, diff --git a/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt b/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt index ab2d1c4..419e4e2 100644 --- a/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt @@ -1,18 +1,10 @@ package li.songe.gkd.data import android.accessibilityservice.AccessibilityService -import android.util.Log import android.view.accessibility.AccessibilityNodeInfo import kotlinx.coroutines.Job -import li.songe.gkd.META -import li.songe.gkd.service.A11yService -import li.songe.gkd.service.createCacheTransform -import li.songe.gkd.service.createNoCacheTransform import li.songe.gkd.service.lastTriggerRule import li.songe.gkd.service.lastTriggerTime -import li.songe.gkd.service.querySelector -import li.songe.gkd.service.safeActiveWindow -import li.songe.gkd.util.ResolvedGroup import li.songe.selector.MatchOption import li.songe.selector.Selector @@ -26,12 +18,13 @@ sealed class ResolvedRule( val config = g.config val key = rule.key val index = group.rules.indexOfFirst { r -> r === rule } + val excludeData = g.excludeData private val preKeys = (rule.preKeys ?: emptyList()).toSet() - private val matches = + val matches = (rule.matches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) } - private val excludeMatches = + val excludeMatches = (rule.excludeMatches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) } - private val anyMatches = + val anyMatches = (rule.anyMatches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) } private val resetMatch = rule.resetMatch ?: group.resetMatch @@ -39,11 +32,11 @@ sealed class ResolvedRule( val actionDelay = rule.actionDelay ?: group.actionDelay ?: 0L private val matchTime = rule.matchTime ?: group.matchTime private val forcedTime = rule.forcedTime ?: group.forcedTime ?: 0L - private val matchOption = MatchOption( + val matchOption = MatchOption( quickFind = rule.quickFind ?: group.quickFind ?: false, fastQuery = rule.fastQuery ?: group.fastQuery ?: false ) - private val matchRoot = rule.matchRoot ?: group.matchRoot ?: false + val matchRoot = rule.matchRoot ?: group.matchRoot ?: false val order = rule.order ?: group.order ?: 0 private val actionCdKey = rule.actionCdKey ?: group.actionCdKey @@ -63,6 +56,18 @@ sealed class ResolvedRule( private val hasSlowSelector by lazy { (matches + excludeMatches + anyMatches).any { s -> s.isSlow(matchOption) } } + val priorityTime = rule.priorityTime ?: group.priorityTime ?: 0 + val priorityActionMaximum = rule.priorityActionMaximum ?: group.priorityActionMaximum ?: 1 + val priorityEnabled: Boolean + get() = priorityTime > 0 + + fun isPriority(): Boolean { + if (!priorityEnabled) return false + if (priorityActionMaximum <= actionCount.value) return false + if (!status.ok) return false + val t = System.currentTimeMillis() + return t - matchChangedTime < priorityTime + matchDelay + } val isSlow by lazy { preKeys.isEmpty() && (matchTime == null || matchTime > 10_000L) && hasSlowSelector } @@ -138,52 +143,6 @@ sealed class ResolvedRule( else -> true } - private val useCache = (matches + excludeMatches + anyMatches).any { s -> s.useCache } - private val transform = if (useCache) defaultCacheTransform else defaultTransform - - fun query( - node: AccessibilityNodeInfo?, - isRootNode: Boolean, - ): AccessibilityNodeInfo? { - val t = System.currentTimeMillis() - if (t - lastCacheTime > MIN_CACHE_INTERVAL) { - clearNodeCache(t) - } - val nodeInfo = if (matchRoot) { - val rootNode = (if (isRootNode) { - node - } else { - A11yService.instance?.safeActiveWindow - }) ?: return null - rootNode.apply { - transform.cache.rootNode = this - } - } else { - node - } - if (nodeInfo == null) return null - var target: AccessibilityNodeInfo? = null - if (anyMatches.isNotEmpty()) { - for (selector in anyMatches) { - target = nodeInfo.querySelector( - selector, matchOption, transform.transform, isRootNode || matchRoot - ) ?: break - } - if (target == null) return null - } - for (selector in matches) { - target = nodeInfo.querySelector( - selector, matchOption, transform.transform, isRootNode || matchRoot - ) ?: return null - } - for (selector in excludeMatches) { - nodeInfo.querySelector( - selector, matchOption, transform.transform, isRootNode || matchRoot - )?.let { return null } - } - return target - } - private val performer = ActionPerformer.getAction(rule.action ?: rule.position?.let { ActionPerformer.ClickCenter.action }) @@ -229,13 +188,10 @@ sealed class ResolvedRule( return "id:${subsItem.id}, v:${rawSubs.version}, type:${type}, gKey=${group.key}, gName:${group.name}, index:${index}, key:${key}, status:${status.name}" } - val excludeData = ExcludeData.parse(config?.exclude) - abstract val type: String // 范围越精确, 优先级越高 abstract fun matchActivity(appId: String, activityId: String? = null): Boolean - } sealed class RuleStatus(val name: String) { @@ -246,13 +202,17 @@ sealed class RuleStatus(val name: String) { data object Status4 : RuleStatus("超出匹配时间") data object Status5 : RuleStatus("处于冷却时间") data object Status6 : RuleStatus("处于点击延迟") + + val ok: Boolean + get() = this === StatusOk } fun getFixActivityIds( appId: String, activityIds: List?, ): List { - return (activityIds ?: emptyList()).map { activityId -> + if (activityIds == null || activityIds.isEmpty()) return emptyList() + return activityIds.map { activityId -> if (activityId.startsWith('.')) { // .a.b.c -> com.x.y.x.a.b.c appId + activityId } else { @@ -260,20 +220,3 @@ fun getFixActivityIds( } } } - -private val defaultTransform by lazy { createNoCacheTransform() } -private val defaultCacheTransform by lazy { createCacheTransform() } -private var lastCacheTime = 0L -private const val MIN_CACHE_INTERVAL = 2000L - -fun clearNodeCache(t: Long = System.currentTimeMillis()) { - lastCacheTime = t - if (META.debuggable) { - val sizeList = defaultCacheTransform.cache.sizeList - if (sizeList.any { it > 0 }) { - Log.d("cache", "clear cache, sizeList=$sizeList") - } - } - defaultTransform.cache.clear() - defaultCacheTransform.cache.clear() -} diff --git a/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt b/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt index fe81296..2153b3b 100644 --- a/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt +++ b/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt @@ -11,7 +11,7 @@ import li.songe.gkd.debug.SnapshotExt.captureSnapshot import li.songe.gkd.service.A11yService import li.songe.gkd.service.TopActivity import li.songe.gkd.service.getAndUpdateCurrentRules -import li.songe.gkd.service.safeActiveWindow +import li.songe.gkd.service.safeActiveWindowAppId import li.songe.gkd.service.updateTopActivity import li.songe.gkd.shizuku.safeGetTopActivity import li.songe.gkd.util.launchTry @@ -27,7 +27,7 @@ class SnapshotTileService : TileService() { return } appScope.launchTry(Dispatchers.IO) { - val oldAppId = service.safeActiveWindow?.packageName?.toString() + val oldAppId = service.safeActiveWindowAppId ?: return@launchTry toast("获取界面信息根节点失败") val startTime = System.currentTimeMillis() @@ -37,7 +37,7 @@ class SnapshotTileService : TileService() { val timeoutText = "没有检测到界面切换,捕获失败" while (true) { - val latestAppId = service.safeActiveWindow?.packageName?.toString() + val latestAppId = service.safeActiveWindowAppId if (latestAppId == null) { // https://github.com/gkd-kit/gkd/issues/713 delay(250) diff --git a/app/src/main/kotlin/li/songe/gkd/service/A11yContext.kt b/app/src/main/kotlin/li/songe/gkd/service/A11yContext.kt new file mode 100644 index 0000000..9e4f669 --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/service/A11yContext.kt @@ -0,0 +1,526 @@ +package li.songe.gkd.service + +import android.graphics.Rect +import android.util.Log +import android.util.LruCache +import android.view.accessibility.AccessibilityNodeInfo +import li.songe.gkd.META +import li.songe.gkd.data.ResolvedRule +import li.songe.gkd.util.InterruptRuleMatchException +import li.songe.selector.Context +import li.songe.selector.FastQuery +import li.songe.selector.MatchOption +import li.songe.selector.Selector +import li.songe.selector.Transform +import li.songe.selector.getBooleanInvoke +import li.songe.selector.getCharSequenceAttr +import li.songe.selector.getCharSequenceInvoke +import li.songe.selector.getIntInvoke + + +private operator fun LruCache.set(child: K, value: V): V { + return put(child, value) +} + +private fun List.getInt(i: Int = 0) = get(i) as Int + +private const val MAX_CACHE_SIZE = MAX_DESCENDANTS_SIZE + +class A11yContext( + private val disableInterrupt: Boolean = false +) { + private var childCache = + LruCache, AccessibilityNodeInfo>(MAX_CACHE_SIZE) + private var indexCache = LruCache(MAX_CACHE_SIZE) + private var parentCache = LruCache(MAX_CACHE_SIZE) + var rootCache: AccessibilityNodeInfo? = null + + private fun clearNodeCache() { + rootCache = null + try { + childCache.evictAll() + parentCache.evictAll() + indexCache.evictAll() + } catch (_: Exception) { + // https://github.com/gkd-kit/gkd/issues/664 + // 在某些机型上 未知原因 缓存不一致 导致删除失败 + childCache = LruCache(MAX_CACHE_SIZE) + indexCache = LruCache(MAX_CACHE_SIZE) + parentCache = LruCache(MAX_CACHE_SIZE) + } + } + + private var lastClearTime = 0L + private fun clearNodeCacheIfTimeout() { + val currentTime = System.currentTimeMillis() + if (currentTime - lastClearTime > 5000L) { + lastClearTime = currentTime + if (META.debuggable) { + val sizeList = listOf(childCache.size(), parentCache.size(), indexCache.size()) + if (sizeList.any { it > 0 }) { + Log.d("cache", "clear cache -> $sizeList") + } + } + clearNodeCache() + } + } + + var currentRule: ResolvedRule? = null + + @Volatile + var interruptKey = 0 + private var interruptInnerKey = 0 + + private fun guardInterrupt() { + if (disableInterrupt) return + if (interruptInnerKey == interruptKey) return + if (!activityRuleFlow.value.activePriority) return + val rule = currentRule ?: return + if (rule.isPriority()) return + interruptInnerKey = interruptKey + if (META.debuggable) { + Log.d("guardInterrupt", "中断 rule=${rule.statusText()}") + } + throw InterruptRuleMatchException() + } + + private fun getA11Root(): AccessibilityNodeInfo? { + guardInterrupt() + return A11yService.instance?.safeActiveWindow + } + + private fun getA11Child(node: AccessibilityNodeInfo, index: Int): AccessibilityNodeInfo? { + guardInterrupt() + return node.getChild(index) + } + + private fun getA11Parent(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { + guardInterrupt() + return node.parent + } + + private fun getA11ByText( + node: AccessibilityNodeInfo, + value: String + ): List { + guardInterrupt() + return node.findAccessibilityNodeInfosByText(value) + } + + private fun getA11ById( + node: AccessibilityNodeInfo, + value: String + ): List { + guardInterrupt() + return node.findAccessibilityNodeInfosByViewId(value) + } + + private fun getFastQueryNodes( + node: AccessibilityNodeInfo, + fastQuery: FastQuery + ): List { + return when (fastQuery) { + is FastQuery.Id -> getA11ById(node, fastQuery.value) + is FastQuery.Text -> getA11ByText(node, fastQuery.value) + is FastQuery.Vid -> getA11ById(node, "${node.packageName}:id/${fastQuery.value}") + } + } + + private fun getCacheRoot(node: AccessibilityNodeInfo? = null): AccessibilityNodeInfo? { + if (rootCache == null) { + rootCache = getA11Root() + } + if (node == rootCache) return null + return rootCache + } + + private fun getPureIndex(node: AccessibilityNodeInfo): Int? { + return indexCache[node] + } + + private fun getCacheIndex(node: AccessibilityNodeInfo): Int { + indexCache[node]?.let { return it } + getCacheParent(node)?.let(::getCacheChildren)?.forEachIndexed { index, child -> + if (child == node) { + indexCache[node] = index + return index + } + } + return 0 + } + + private fun getCacheParent(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { + if (rootCache == node) { + return null + } + parentCache[node]?.let { return it } + return getA11Parent(node).apply { + if (this != null) { + parentCache[node] = this + } else { + rootCache = node + } + } + } + + /** + * 在无缓存时, 此方法小概率造成无限节点片段,底层原因未知 + * + * https://github.com/gkd-kit/gkd/issues/28 + */ + private fun getCacheDepth(node: AccessibilityNodeInfo): Int { + var p: AccessibilityNodeInfo = node + var depth = 0 + while (true) { + val p2 = getCacheParent(p) + if (p2 != null) { + p = p2 + depth++ + } else { + break + } + } + return depth + } + + private fun getCacheChild(node: AccessibilityNodeInfo, index: Int): AccessibilityNodeInfo? { + if (index !in 0 until node.childCount) { + return null + } + return childCache[node to index] ?: getA11Child(node, index)?.also { child -> + indexCache[child] = index + parentCache[child] = node + childCache[node to index] = child + } + } + + private fun getCacheChildren(node: AccessibilityNodeInfo): Sequence { + return sequence { + repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { index -> + val child = getCacheChild(node, index) ?: return@sequence + yield(child) + } + } + } + + private var tempNode: AccessibilityNodeInfo? = null + private val tempRect = Rect() + private var tempVid: CharSequence? = null + private fun getTempRect(n: AccessibilityNodeInfo): Rect { + if (n !== tempNode) { + n.getBoundsInScreen(tempRect) + tempNode = n + } + return tempRect + } + + private fun getTempVid(n: AccessibilityNodeInfo): CharSequence? { + if (n !== tempNode) { + tempVid = n.getVid() + tempNode = n + } + return tempVid + } + + private fun getCacheAttr(node: AccessibilityNodeInfo, name: String): Any? = when (name) { + "id" -> node.viewIdResourceName + "vid" -> getTempVid(node) + + "name" -> node.className + "text" -> node.text + "desc" -> node.contentDescription + + "clickable" -> node.isClickable + "focusable" -> node.isFocusable + "checkable" -> node.isCheckable + "checked" -> node.isChecked + "editable" -> node.isEditable + "longClickable" -> node.isLongClickable + "visibleToUser" -> node.isVisibleToUser + + "left" -> getTempRect(node).left + "top" -> getTempRect(node).top + "right" -> getTempRect(node).right + "bottom" -> getTempRect(node).bottom + + "width" -> getTempRect(node).width() + "height" -> getTempRect(node).height() + + "index" -> getCacheIndex(node) + "depth" -> getCacheDepth(node) + "childCount" -> node.childCount + + "parent" -> getCacheParent(node) + + else -> null + } + + private val transform = Transform( + getAttr = { target, name -> + when (target) { + is Context<*> -> when (name) { + "prev" -> target.prev + "current" -> target.current + else -> getCacheAttr(target.current as AccessibilityNodeInfo, name) + } + + is AccessibilityNodeInfo -> getCacheAttr(target, name) + is CharSequence -> getCharSequenceAttr(target, name) + else -> null + } + }, + getInvoke = { target, name, args -> + when (target) { + is AccessibilityNodeInfo -> when (name) { + "getChild" -> { + getCacheChild(target, args.getInt()) + } + + else -> null + } + + is Context<*> -> when (name) { + "getPrev" -> { + args.getInt().let { target.getPrev(it) } + } + + "getChild" -> { + getCacheChild(target.current as AccessibilityNodeInfo, args.getInt()) + } + + else -> null + } + + is CharSequence -> getCharSequenceInvoke(target, name, args) + is Int -> getIntInvoke(target, name, args) + is Boolean -> getBooleanInvoke(target, name, args) + + else -> null + } + + }, + getName = { node -> node.className }, + getChildren = ::getCacheChildren, + getParent = ::getCacheParent, + getRoot = ::getCacheRoot, + getDescendants = { node -> + sequence { + val stack = getCacheChildren(node).toMutableList() + if (stack.isEmpty()) return@sequence + stack.reverse() + val tempNodes = mutableListOf() + do { + val top = stack.removeAt(stack.lastIndex) + yield(top) + for (childNode in getCacheChildren(top)) { + tempNodes.add(childNode) + } + if (tempNodes.isNotEmpty()) { + for (i in tempNodes.size - 1 downTo 0) { + stack.add(tempNodes[i]) + } + tempNodes.clear() + } + } while (stack.isNotEmpty()) + }.take(MAX_DESCENDANTS_SIZE) + }, + traverseChildren = { node, connectExpression -> + sequence { + repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { offset -> + connectExpression.maxOffset?.let { maxOffset -> + if (offset > maxOffset) return@sequence + } + if (connectExpression.checkOffset(offset)) { + val child = getCacheChild(node, offset) ?: return@sequence + yield(child) + } + } + } + }, + traverseBeforeBrothers = { node, connectExpression -> + sequence { + val parentVal = getCacheParent(node) ?: return@sequence + // 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 cache.index 是空 + val index = getPureIndex(node) + if (index != null) { + var i = index - 1 + var offset = 0 + while (0 <= i && i < parentVal.childCount) { + connectExpression.maxOffset?.let { maxOffset -> + if (offset > maxOffset) return@sequence + } + if (connectExpression.checkOffset(offset)) { + val child = getCacheChild(parentVal, i) ?: return@sequence + yield(child) + } + i-- + offset++ + } + } else { + val list = getCacheChildren(parentVal).takeWhile { it != node }.toMutableList() + list.reverse() + yieldAll(list.filterIndexed { i, _ -> + connectExpression.checkOffset( + i + ) + }) + } + } + }, + traverseAfterBrothers = { node, connectExpression -> + val parentVal = getCacheParent(node) + if (parentVal != null) { + val index = getPureIndex(node) + if (index != null) { + sequence { + var i = index + 1 + var offset = 0 + while (0 <= i && i < parentVal.childCount) { + connectExpression.maxOffset?.let { maxOffset -> + if (offset > maxOffset) return@sequence + } + if (connectExpression.checkOffset(offset)) { + val child = getCacheChild(parentVal, i) ?: return@sequence + yield(child) + } + i++ + offset++ + } + } + } else { + getCacheChildren(parentVal).dropWhile { it != node } + .drop(1) + .let { + if (connectExpression.maxOffset != null) { + it.take(connectExpression.maxOffset!! + 1) + } else { + it + } + } + .filterIndexed { i, _ -> + connectExpression.checkOffset( + i + ) + } + } + } else { + emptySequence() + } + }, + traverseDescendants = { node, connectExpression -> + sequence { + val stack = getCacheChildren(node).toMutableList() + if (stack.isEmpty()) return@sequence + stack.reverse() + val tempNodes = mutableListOf() + var offset = 0 + do { + val top = stack.removeAt(stack.lastIndex) + if (connectExpression.checkOffset(offset)) { + yield(top) + } + offset++ + if (offset > MAX_DESCENDANTS_SIZE) { + return@sequence + } + connectExpression.maxOffset?.let { maxOffset -> + if (offset > maxOffset) return@sequence + } + for (childNode in getCacheChildren(top)) { + tempNodes.add(childNode) + } + if (tempNodes.isNotEmpty()) { + for (i in tempNodes.size - 1 downTo 0) { + stack.add(tempNodes[i]) + } + tempNodes.clear() + } + } while (stack.isNotEmpty()) + } + }, + traverseFastQueryDescendants = { node, fastQueryList -> + sequence { + for (fastQuery in fastQueryList) { + val nodes = getFastQueryNodes(node, fastQuery) + nodes.forEach { childNode -> + yield(childNode) + } + } + } + } + ) + + fun querySelector( + node: AccessibilityNodeInfo, + selector: Selector, + option: MatchOption, + ): AccessibilityNodeInfo? { + if (selector.isMatchRoot) { + return selector.match( + getCacheRoot() ?: return null, + transform, + option + ) + } + if (option.fastQuery && selector.fastQueryList.isNotEmpty()) { + val nodes = transform.traverseFastQueryDescendants(node, selector.fastQueryList) + nodes.forEach { childNode -> + selector.match(childNode, transform, option)?.let { return it } + } + return null + } + if (option.quickFind && selector.quickFindValue != null) { + val nodes = getFastQueryNodes(node, selector.quickFindValue!!) + nodes.forEach { childNode -> + selector.match(childNode, transform, option)?.let { return it } + } + return null + } + return transform.querySelector(node, selector, option) + } + + fun queryRule( + rule: ResolvedRule, + node: AccessibilityNodeInfo, + ): AccessibilityNodeInfo? { + clearNodeCacheIfTimeout() + currentRule = rule + try { + val queryNode = if (rule.matchRoot) { + getCacheRoot() + } else { + node + } ?: return null + var resultNode: AccessibilityNodeInfo? = null + if (rule.anyMatches.isNotEmpty()) { + for (selector in rule.anyMatches) { + resultNode = a11yContext.querySelector( + queryNode, + selector, + rule.matchOption, + ) ?: break + } + if (resultNode == null) return null + } + for (selector in rule.matches) { + resultNode = a11yContext.querySelector( + queryNode, + selector, + rule.matchOption, + ) ?: return null + } + for (selector in rule.excludeMatches) { + a11yContext.querySelector( + queryNode, + selector, + rule.matchOption, + )?.let { return null } + } + return resultNode + } finally { + currentRule = null + } + } +} + +val a11yContext = A11yContext() diff --git a/app/src/main/kotlin/li/songe/gkd/service/A11yExt.kt b/app/src/main/kotlin/li/songe/gkd/service/A11yExt.kt deleted file mode 100644 index e49831b..0000000 --- a/app/src/main/kotlin/li/songe/gkd/service/A11yExt.kt +++ /dev/null @@ -1,649 +0,0 @@ -package li.songe.gkd.service - -import android.accessibilityservice.AccessibilityService -import android.graphics.Rect -import android.util.LruCache -import android.view.accessibility.AccessibilityEvent -import android.view.accessibility.AccessibilityNodeInfo -import com.blankj.utilcode.util.LogUtils -import li.songe.gkd.META -import li.songe.selector.Context -import li.songe.selector.FastQuery -import li.songe.selector.MatchOption -import li.songe.selector.MismatchExpressionTypeException -import li.songe.selector.MismatchOperatorTypeException -import li.songe.selector.MismatchParamTypeException -import li.songe.selector.Selector -import li.songe.selector.Transform -import li.songe.selector.UnknownIdentifierException -import li.songe.selector.UnknownIdentifierMethodException -import li.songe.selector.UnknownIdentifierMethodParamsException -import li.songe.selector.UnknownMemberException -import li.songe.selector.UnknownMemberMethodException -import li.songe.selector.UnknownMemberMethodParamsException -import li.songe.selector.getBooleanInvoke -import li.songe.selector.getCharSequenceAttr -import li.songe.selector.getCharSequenceInvoke -import li.songe.selector.getIntInvoke -import li.songe.selector.initDefaultTypeInfo - -// 某些应用耗时 554ms -val AccessibilityService.safeActiveWindow: AccessibilityNodeInfo? - get() = try { - // java.lang.SecurityException: Call from user 0 as user -2 without permission INTERACT_ACROSS_USERS or INTERACT_ACROSS_USERS_FULL not allowed. - rootInActiveWindow - // 在主线程调用会阻塞界面导致卡顿 - } catch (e: Exception) { - e.printStackTrace() - null - } - -val AccessibilityService.activeWindowAppId: String? - get() = safeActiveWindow?.packageName?.toString() - -// 在某些应用耗时 300ms -val AccessibilityEvent.safeSource: AccessibilityNodeInfo? - get() = if (className == null) { - null // https://github.com/gkd-kit/gkd/issues/426 event.clear 已被系统调用 - } else { - try { - // 仍然报错 Cannot perform this action on a not sealed instance. - // TODO 原因未知 - source - } catch (e: Exception) { - null - } - } - -inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode: AccessibilityNodeInfo?) -> Unit) { - var index = 0 - val childCount = this.childCount - while (index < childCount) { - val child: AccessibilityNodeInfo? = getChild(index) - action(index, child) - index += 1 - } -} - -fun AccessibilityNodeInfo.getVid(): CharSequence? { - val id = viewIdResourceName ?: return null - val appId = packageName ?: return null - if (id.startsWith(appId) && id.startsWith(":id/", appId.length)) { - return id.subSequence( - appId.length + ":id/".length, - id.length - ) - } - return null -} - -fun AccessibilityNodeInfo.querySelector( - selector: Selector, - option: MatchOption, - transform: Transform, - isRootNode: Boolean, -): AccessibilityNodeInfo? { - if (selector.isMatchRoot) { - val root = if (isRootNode) { - return this - } else { - A11yService.instance?.safeActiveWindow ?: return null - } - return selector.match(root, transform, option) - } - if (option.fastQuery && selector.fastQueryList.isNotEmpty()) { - val nodes = transform.traverseFastQueryDescendants(this, selector.fastQueryList) - nodes.forEach { childNode -> - val targetNode = selector.match(childNode, transform, option) - if (targetNode != null) return targetNode - } - return null - } - if (option.quickFind && selector.quickFindValue != null) { - val nodes = getFastQueryNodes(this, selector.quickFindValue!!) - nodes.forEach { childNode -> - val targetNode = selector.match(childNode, transform, option) - if (targetNode != null) return targetNode - } - return null - } - // 在一些开屏广告的界面会造成1-2s的阻塞 - return transform.querySelector(this, selector, option) -} - -private fun getFastQueryNodes( - node: AccessibilityNodeInfo, - fastQuery: FastQuery -): List { - return when (fastQuery) { - is FastQuery.Id -> node.findAccessibilityNodeInfosByViewId(fastQuery.value) - is FastQuery.Text -> node.findAccessibilityNodeInfosByText(fastQuery.value) - is FastQuery.Vid -> node.findAccessibilityNodeInfosByViewId("${node.packageName}:id/${fastQuery.value}") - } -} - -private fun traverseFastQueryDescendants( - node: AccessibilityNodeInfo, - fastQueryList: List -): Sequence { - return sequence { - for (fastQuery in fastQueryList) { - val nodes = getFastQueryNodes(node, fastQuery) - nodes.forEach { childNode -> - yield(childNode) - } - } - } -} - - -// https://github.com/gkd-kit/gkd/issues/115 -// https://github.com/gkd-kit/gkd/issues/650 -// 限制节点遍历的数量避免内存溢出 -private const val MAX_CHILD_SIZE = 512 -private const val MAX_DESCENDANTS_SIZE = 4096 - -val getChildren: (AccessibilityNodeInfo) -> Sequence = { node -> - sequence { - repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { i -> - val child = node.getChild(i) ?: return@sequence - yield(child) - } - } -} - -private val typeInfo by lazy { - initDefaultTypeInfo().globalType -} - -fun Selector.checkSelector(): String? { - val error = checkType(typeInfo) ?: return null - if (META.debuggable) { - LogUtils.d( - "Selector check error", - source, - error.message - ) - } - return when (error) { - is MismatchExpressionTypeException -> "不匹配表达式类型:${error.exception.stringify()}" - is MismatchOperatorTypeException -> "不匹配操作符类型:${error.exception.stringify()}" - is MismatchParamTypeException -> "不匹配参数类型:${error.call.stringify()}" - is UnknownIdentifierException -> "未知属性:${error.value.stringify()}" - is UnknownIdentifierMethodException -> "未知方法:${error.value.stringify()}" - is UnknownMemberException -> "未知属性:${error.value.stringify()}" - is UnknownMemberMethodException -> "未知方法:${error.value.stringify()}" - is UnknownIdentifierMethodParamsException -> "未知方法参数:${error.value.stringify()}" - is UnknownMemberMethodParamsException -> "未知方法参数:${error.value.stringify()}" - } -} - -private fun createGetNodeAttr(cache: NodeCache): ((AccessibilityNodeInfo, String) -> Any?) { - var tempNode: AccessibilityNodeInfo? = null - val tempRect = Rect() - var tempVid: CharSequence? = null - fun AccessibilityNodeInfo.getTempRect(): Rect { - if (this !== tempNode) { - getBoundsInScreen(tempRect) - tempNode = this - } - return tempRect - } - - fun AccessibilityNodeInfo.getTempVid(): CharSequence? { - if (this !== tempNode) { - tempVid = getVid() - tempNode = this - } - return tempVid - } - - return { node, name -> - when (name) { - "id" -> node.viewIdResourceName - "vid" -> node.getTempVid() - - "name" -> node.className - "text" -> node.text - "desc" -> node.contentDescription - - "clickable" -> node.isClickable - "focusable" -> node.isFocusable - "checkable" -> node.isCheckable - "checked" -> node.isChecked - "editable" -> node.isEditable - "longClickable" -> node.isLongClickable - "visibleToUser" -> node.isVisibleToUser - - "left" -> node.getTempRect().left - "top" -> node.getTempRect().top - "right" -> node.getTempRect().right - "bottom" -> node.getTempRect().bottom - - "width" -> node.getTempRect().width() - "height" -> node.getTempRect().height() - - "index" -> cache.getIndex(node) - "depth" -> cache.getDepth(node) - "childCount" -> node.childCount - - "parent" -> cache.getParent(node) - - else -> null - } - } -} - -data class CacheTransform( - val transform: Transform, - val cache: NodeCache, -) - - -private operator fun LruCache.set(child: K, value: V): V { - return put(child, value) -} - -private const val MAX_CACHE_SIZE = MAX_DESCENDANTS_SIZE - -class NodeCache { - private var childMap = - LruCache, AccessibilityNodeInfo>(MAX_CACHE_SIZE) - private var indexMap = LruCache(MAX_CACHE_SIZE) - private var parentMap = LruCache(MAX_CACHE_SIZE) - var rootNode: AccessibilityNodeInfo? = null - - fun clear() { - rootNode = null - try { - childMap.evictAll() - parentMap.evictAll() - indexMap.evictAll() - } catch (e: Exception) { - // https://github.com/gkd-kit/gkd/issues/664 - // 在某些机型上 未知原因 缓存不一致 导致删除失败 - childMap = LruCache(MAX_CACHE_SIZE) - indexMap = LruCache(MAX_CACHE_SIZE) - parentMap = LruCache(MAX_CACHE_SIZE) - } - } - - fun getRoot(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { - if (rootNode == null) { - rootNode = A11yService.instance?.safeActiveWindow - } - if (node == rootNode) return null - return rootNode - } - - val sizeList: List - get() = listOf(childMap.size(), parentMap.size(), indexMap.size()) - - fun getPureIndex(node: AccessibilityNodeInfo): Int? { - return indexMap[node] - } - - fun getIndex(node: AccessibilityNodeInfo): Int { - indexMap[node]?.let { return it } - getParent(node)?.forEachIndexed { index, child -> - if (child != null) { - indexMap[child] = index - } - if (child == node) { - return index - } - } - return 0 - } - - fun getParent(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { - if (rootNode == node) { - return null - } - val parent = parentMap[node] - if (parent != null) { - return parent - } - return node.parent.apply { - if (this != null) { - parentMap[node] = this - } else { - rootNode = node - } - } - } - - /** - * 在无缓存时, 此方法小概率造成无限节点片段,底层原因未知 - * - * https://github.com/gkd-kit/gkd/issues/28 - */ - fun getDepth(node: AccessibilityNodeInfo): Int { - var p: AccessibilityNodeInfo = node - var depth = 0 - while (true) { - val p2 = getParent(p) - if (p2 != null) { - p = p2 - depth++ - } else { - break - } - } - return depth - } - - fun getChild(node: AccessibilityNodeInfo, index: Int): AccessibilityNodeInfo? { - if (index !in 0 until node.childCount) { - return null - } - return childMap[node to index] ?: node.getChild(index)?.also { child -> - indexMap[child] = index - parentMap[child] = node - childMap[node to index] = child - } - } -} - -fun createCacheTransform(): CacheTransform { - val cache = NodeCache() - - val getChildrenCache: (AccessibilityNodeInfo) -> Sequence = { node -> - sequence { - repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { index -> - val child = cache.getChild(node, index) ?: return@sequence - yield(child) - } - } - } - val getNodeAttr = createGetNodeAttr(cache) - val transform = Transform( - getAttr = { target, name -> - when (target) { - is Context<*> -> when (name) { - "prev" -> target.prev - "current" -> target.current - else -> getNodeAttr(target.current as AccessibilityNodeInfo, name) - } - - is AccessibilityNodeInfo -> getNodeAttr(target, name) - is CharSequence -> getCharSequenceAttr(target, name) - else -> null - } - }, - getInvoke = { target, name, args -> - when (target) { - is AccessibilityNodeInfo -> when (name) { - "getChild" -> { - args.getInt().let { index -> - cache.getChild(target, index) - } - } - - else -> null - } - - is Context<*> -> when (name) { - "getPrev" -> { - args.getInt().let { target.getPrev(it) } - } - - "getChild" -> { - args.getInt().let { index -> - cache.getChild(target.current as AccessibilityNodeInfo, index) - } - } - - else -> null - } - - is CharSequence -> getCharSequenceInvoke(target, name, args) - is Int -> getIntInvoke(target, name, args) - is Boolean -> getBooleanInvoke(target, name, args) - - else -> null - } - - }, - getName = { node -> node.className }, - getChildren = getChildrenCache, - getParent = { cache.getParent(it) }, - getRoot = { cache.getRoot(it) }, - getDescendants = { node -> - sequence { - val stack = getChildrenCache(node).toMutableList() - if (stack.isEmpty()) return@sequence - stack.reverse() - val tempNodes = mutableListOf() - do { - val top = stack.removeAt(stack.lastIndex) - yield(top) - for (childNode in getChildrenCache(top)) { - tempNodes.add(childNode) - } - if (tempNodes.isNotEmpty()) { - for (i in tempNodes.size - 1 downTo 0) { - stack.add(tempNodes[i]) - } - tempNodes.clear() - } - } while (stack.isNotEmpty()) - }.take(MAX_DESCENDANTS_SIZE) - }, - traverseChildren = { node, connectExpression -> - sequence { - repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { offset -> - connectExpression.maxOffset?.let { maxOffset -> - if (offset > maxOffset) return@sequence - } - if (connectExpression.checkOffset(offset)) { - val child = cache.getChild(node, offset) ?: return@sequence - yield(child) - } - } - } - }, - traverseBeforeBrothers = { node, connectExpression -> - sequence { - val parentVal = cache.getParent(node) ?: return@sequence - // 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 cache.index 是空 - val index = cache.getPureIndex(node) - if (index != null) { - var i = index - 1 - var offset = 0 - while (0 <= i && i < parentVal.childCount) { - connectExpression.maxOffset?.let { maxOffset -> - if (offset > maxOffset) return@sequence - } - if (connectExpression.checkOffset(offset)) { - val child = cache.getChild(parentVal, i) ?: return@sequence - yield(child) - } - i-- - offset++ - } - } else { - val list = - getChildrenCache(parentVal).takeWhile { it != node }.toMutableList() - list.reverse() - yieldAll(list.filterIndexed { i, _ -> - connectExpression.checkOffset( - i - ) - }) - } - } - }, - traverseAfterBrothers = { node, connectExpression -> - val parentVal = cache.getParent(node) - if (parentVal != null) { - val index = cache.getPureIndex(node) - if (index != null) { - sequence { - var i = index + 1 - var offset = 0 - while (0 <= i && i < parentVal.childCount) { - connectExpression.maxOffset?.let { maxOffset -> - if (offset > maxOffset) return@sequence - } - if (connectExpression.checkOffset(offset)) { - val child = cache.getChild(parentVal, i) ?: return@sequence - yield(child) - } - i++ - offset++ - } - } - } else { - getChildrenCache(parentVal).dropWhile { it != node } - .drop(1) - .let { - if (connectExpression.maxOffset != null) { - it.take(connectExpression.maxOffset!! + 1) - } else { - it - } - } - .filterIndexed { i, _ -> - connectExpression.checkOffset( - i - ) - } - } - } else { - emptySequence() - } - }, - traverseDescendants = { node, connectExpression -> - sequence { - val stack = getChildrenCache(node).toMutableList() - if (stack.isEmpty()) return@sequence - stack.reverse() - val tempNodes = mutableListOf() - var offset = 0 - do { - val top = stack.removeAt(stack.lastIndex) - if (connectExpression.checkOffset(offset)) { - yield(top) - } - offset++ - if (offset > MAX_DESCENDANTS_SIZE) { - return@sequence - } - connectExpression.maxOffset?.let { maxOffset -> - if (offset > maxOffset) return@sequence - } - for (childNode in getChildrenCache(top)) { - tempNodes.add(childNode) - } - if (tempNodes.isNotEmpty()) { - for (i in tempNodes.size - 1 downTo 0) { - stack.add(tempNodes[i]) - } - tempNodes.clear() - } - } while (stack.isNotEmpty()) - } - }, - traverseFastQueryDescendants = ::traverseFastQueryDescendants - ) - - return CacheTransform(transform, cache) -} - -private fun List.getInt(i: Int = 0) = get(i) as Int - -fun createNoCacheTransform(): CacheTransform { - val cache = NodeCache() - val getNodeAttr = createGetNodeAttr(cache) - val transform = Transform( - getAttr = { target, name -> - when (target) { - is Context<*> -> when (name) { - "prev" -> target.prev - else -> getNodeAttr(target.current as AccessibilityNodeInfo, name) - } - - is AccessibilityNodeInfo -> getNodeAttr(target, name) - is CharSequence -> getCharSequenceAttr(target, name) - else -> null - } - }, - getInvoke = { target, name, args -> - when (target) { - is AccessibilityNodeInfo -> when (name) { - "getChild" -> { - args.getInt().let { index -> - cache.getChild(target, index) - } - } - - else -> null - } - - is Context<*> -> when (name) { - "getPrev" -> { - args.getInt().let { target.getPrev(it) } - } - - "getChild" -> { - args.getInt().let { index -> - cache.getChild(target.current as AccessibilityNodeInfo, index) - } - } - - else -> null - } - - is CharSequence -> getCharSequenceInvoke(target, name, args) - is Int -> getIntInvoke(target, name, args) - is Boolean -> getBooleanInvoke(target, name, args) - else -> null - } - }, - getName = { node -> node.className }, - getChildren = getChildren, - getParent = { node -> node.parent }, - getDescendants = { node -> - sequence { - val stack = getChildren(node).toMutableList() - if (stack.isEmpty()) return@sequence - stack.reverse() - val tempNodes = mutableListOf() - var offset = 0 - do { - val top = stack.removeAt(stack.lastIndex) - yield(top) - offset++ - if (offset > MAX_DESCENDANTS_SIZE) { - return@sequence - } - for (childNode in getChildren(top)) { - tempNodes.add(childNode) - } - if (tempNodes.isNotEmpty()) { - for (i in tempNodes.size - 1 downTo 0) { - stack.add(tempNodes[i]) - } - tempNodes.clear() - } - } while (stack.isNotEmpty()) - } - }, - traverseChildren = { node, connectExpression -> - sequence { - repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { offset -> - connectExpression.maxOffset?.let { maxOffset -> - if (offset > maxOffset) return@sequence - } - if (connectExpression.checkOffset(offset)) { - val child = node.getChild(offset) ?: return@sequence - yield(child) - } - } - } - }, - traverseFastQueryDescendants = ::traverseFastQueryDescendants - ) - return CacheTransform(transform, cache) -} diff --git a/app/src/main/kotlin/li/songe/gkd/service/A11yService.kt b/app/src/main/kotlin/li/songe/gkd/service/A11yService.kt index 5adbd91..a037655 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/A11yService.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/A11yService.kt @@ -41,7 +41,6 @@ import li.songe.gkd.data.GkdAction import li.songe.gkd.data.ResolvedRule import li.songe.gkd.data.RpcError import li.songe.gkd.data.RuleStatus -import li.songe.gkd.data.clearNodeCache import li.songe.gkd.debug.SnapshotExt import li.songe.gkd.permission.shizukuOkState import li.songe.gkd.shizuku.safeGetTopActivity @@ -85,10 +84,9 @@ class A11yService : AccessibilityService(), OnCreate, OnA11yConnected, OnA11yEve override fun onDestroy() { super.onDestroy() onDestroyed() - scope.cancel() } - val scope = CoroutineScope(Dispatchers.Default) + val scope = CoroutineScope(Dispatchers.Default).apply { onDestroyed { cancel() } } init { useRunningState() @@ -119,12 +117,20 @@ class A11yService : AccessibilityService(), OnCreate, OnA11yConnected, OnA11yEve selector.checkSelector()?.let { throw RpcError(it) } - val targetNode = serviceVal.safeActiveWindow?.querySelector( - selector, MatchOption( - quickFind = gkdAction.quickFind, - fastQuery = gkdAction.fastQuery, - ), createCacheTransform().transform, isRootNode = true - ) ?: throw RpcError("没有查询到节点") + val matchOption = MatchOption( + quickFind = gkdAction.quickFind, + fastQuery = gkdAction.fastQuery, + ) + val cache = A11yContext(true) + + val targetNode = serviceVal.safeActiveWindow?.let { + cache.rootCache = it + cache.querySelector( + it, + selector, + matchOption + ) + } ?: throw RpcError("没有查询到节点") if (gkdAction.action == null) { // 仅查询 @@ -198,17 +204,18 @@ private fun A11yService.useMatchRule() { } null } else { - if (META.debuggable) { - Log.d( - "queryEvents", - "保留最后两个事件:${queryEvents.first().appId}${queryEvents.map { it.className }}" - ) - } // type,appId,className 一致, 需要在 synchronized 外验证是否是同一节点 arrayOf( queryEvents[queryEvents.size - 2], queryEvents.last(), - ) + ).apply { + if (META.debuggable) { + Log.d( + "queryEvents", + "保留最后两个事件:${queryEvents.first().appId}=${map { it.className }}" + ) + } + } } } else if (queryEvents.size == 1) { if (META.debuggable) { @@ -246,8 +253,7 @@ private fun A11yService.useMatchRule() { } } var lastNodeUsed = false - clearNodeCache() - for (rule in activityRule.currentRules) { // 规则数量有可能过多导致耗时过长 + for (rule in activityRule.priorityRules) { // 规则数量有可能过多导致耗时过长 if (delayRule != null && delayRule !== rule) continue val statusCode = rule.status if (statusCode == RuleStatus.Status3 && rule.matchDelayJob == null) { @@ -299,7 +305,7 @@ private fun A11yService.useMatchRule() { return@launchQuery } if (!matchApp) continue - val target = rule.query(nodeVal, lastNode == null) ?: continue + val target = a11yContext.queryRule(rule, nodeVal) ?: continue if (activityRule !== getAndUpdateCurrentRules()) break if (rule.checkDelay() && rule.actionDelayJob == null) { rule.actionDelayJob = scope.launch(A11yService.actionThread) { @@ -364,10 +370,10 @@ private fun A11yService.useMatchRule() { // https://github.com/gkd-kit/gkd/issues/622 lastGetAppIdTime = t lastAppId = if (storeFlow.value.enableShizukuActivity) { - withTimeoutOrNull(100) { activeWindowAppId } ?: safeGetTopActivity()?.appId + withTimeoutOrNull(100) { safeActiveWindowAppId } ?: safeGetTopActivity()?.appId } else { null - } ?: activeWindowAppId + } ?: safeActiveWindowAppId } return lastAppId } @@ -438,6 +444,7 @@ private fun A11yService.useMatchRule() { } synchronized(queryEvents) { queryEvents.addAll(consumedEvents) } + a11yContext.interruptKey++ newQueryTask(a11yEvent) } diff --git a/app/src/main/kotlin/li/songe/gkd/service/A11yState.kt b/app/src/main/kotlin/li/songe/gkd/service/A11yState.kt index 10dcbe6..ffda140 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/A11yState.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/A11yState.kt @@ -31,6 +31,10 @@ data class TopActivity( fun format(): String { return "${appId}/${activityId}/${number}" } + + fun sameAs(other: TopActivity): Boolean { + return appId == other.appId && activityId == other.activityId + } } val topActivityFlow = MutableStateFlow(TopActivity()) @@ -39,17 +43,16 @@ private val activityLogMutex by lazy { Mutex() } private var activityLogCount = 0 private var lastActivityChangeTime = 0L fun updateTopActivity(topActivity: TopActivity) { - val isSameActivity = - topActivityFlow.value.appId == topActivity.appId && topActivityFlow.value.activityId == topActivity.activityId + val isSameActivity = topActivityFlow.value.sameAs(topActivity) if (isSameActivity) { - if (isActivityVisible() && topActivity.appId == META.appId) { - return - } if (topActivityFlow.value.number == topActivity.number) { return } + if (isActivityVisible() && topActivity.appId == META.appId) { + return + } val t = System.currentTimeMillis() - if (t - lastActivityChangeTime < 1000) { + if (t - lastActivityChangeTime < 1500) { return } } @@ -76,13 +79,22 @@ fun updateTopActivity(topActivity: TopActivity) { lastActivityChangeTime = System.currentTimeMillis() } -data class ActivityRule( +class ActivityRule( val appRules: List = emptyList(), val globalRules: List = emptyList(), val topActivity: TopActivity = TopActivity(), val ruleSummary: RuleSummary = RuleSummary(), ) { - val currentRules = (appRules + globalRules).sortedBy { r -> r.order } + val currentRules = (appRules + globalRules).sortedBy { it.order } + val hasPriorityRule = currentRules.size > 1 && currentRules.any { it.priorityEnabled } + val activePriority: Boolean + get() = hasPriorityRule && currentRules.any { it.isPriority() } + val priorityRules: List + get() = if (hasPriorityRule) { + currentRules.sortedBy { if (it.isPriority()) 0 else 1 } + } else { + currentRules + } } val activityRuleFlow by lazy { MutableStateFlow(ActivityRule()) } diff --git a/app/src/main/kotlin/li/songe/gkd/service/NodeExt.kt b/app/src/main/kotlin/li/songe/gkd/service/NodeExt.kt new file mode 100644 index 0000000..2107d7b --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/service/NodeExt.kt @@ -0,0 +1,89 @@ +package li.songe.gkd.service + +import android.accessibilityservice.AccessibilityService +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import com.blankj.utilcode.util.LogUtils +import li.songe.gkd.META +import li.songe.selector.MismatchExpressionTypeException +import li.songe.selector.MismatchOperatorTypeException +import li.songe.selector.MismatchParamTypeException +import li.songe.selector.Selector +import li.songe.selector.UnknownIdentifierException +import li.songe.selector.UnknownIdentifierMethodException +import li.songe.selector.UnknownIdentifierMethodParamsException +import li.songe.selector.UnknownMemberException +import li.songe.selector.UnknownMemberMethodException +import li.songe.selector.UnknownMemberMethodParamsException +import li.songe.selector.initDefaultTypeInfo + +// 某些应用耗时 554ms +val AccessibilityService.safeActiveWindow: AccessibilityNodeInfo? + get() = try { + // java.lang.SecurityException: Call from user 0 as user -2 without permission INTERACT_ACROSS_USERS or INTERACT_ACROSS_USERS_FULL not allowed. + rootInActiveWindow.apply { + a11yContext.rootCache = this + } + // 在主线程调用会阻塞界面导致卡顿 + } catch (e: Exception) { + e.printStackTrace() + null + } + +val AccessibilityService.safeActiveWindowAppId: String? + get() = safeActiveWindow?.packageName?.toString() + +// 某些应用耗时 300ms +val AccessibilityEvent.safeSource: AccessibilityNodeInfo? + get() = if (className == null) { + null // https://github.com/gkd-kit/gkd/issues/426 event.clear 已被系统调用 + } else { + try { + // 原因未知, 仍然报错 Cannot perform this action on a not sealed instance. + source + } catch (_: Exception) { + null + } + } + +fun AccessibilityNodeInfo.getVid(): CharSequence? { + val id = viewIdResourceName ?: return null + val appId = packageName ?: return null + if (id.startsWith(appId) && id.startsWith(":id/", appId.length)) { + return id.subSequence( + appId.length + ":id/".length, + id.length + ) + } + return null +} + +// https://github.com/gkd-kit/gkd/issues/115 +// https://github.com/gkd-kit/gkd/issues/650 +// 限制节点遍历的数量避免内存溢出 +const val MAX_CHILD_SIZE = 512 +const val MAX_DESCENDANTS_SIZE = 4096 + +private val typeInfo by lazy { initDefaultTypeInfo().globalType } + +fun Selector.checkSelector(): String? { + val error = checkType(typeInfo) ?: return null + if (META.debuggable) { + LogUtils.d( + "Selector check error", + source, + error.message + ) + } + return when (error) { + is MismatchExpressionTypeException -> "不匹配表达式类型:${error.exception.stringify()}" + is MismatchOperatorTypeException -> "不匹配操作符类型:${error.exception.stringify()}" + is MismatchParamTypeException -> "不匹配参数类型:${error.call.stringify()}" + is UnknownIdentifierException -> "未知属性:${error.value.stringify()}" + is UnknownIdentifierMethodException -> "未知方法:${error.value.stringify()}" + is UnknownMemberException -> "未知属性:${error.value.stringify()}" + is UnknownMemberMethodException -> "未知方法:${error.value.stringify()}" + is UnknownIdentifierMethodParamsException -> "未知方法参数:${error.value.stringify()}" + is UnknownMemberMethodParamsException -> "未知方法参数:${error.value.stringify()}" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AppConfigPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/AppConfigPage.kt index 81f6a23..2ceaa9e 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/AppConfigPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/AppConfigPage.kt @@ -57,6 +57,7 @@ import kotlinx.coroutines.flow.update import li.songe.gkd.MainActivity import li.songe.gkd.data.ExcludeData import li.songe.gkd.data.RawSubscription +import li.songe.gkd.data.ResolvedGroup import li.songe.gkd.data.SubsConfig import li.songe.gkd.data.stringify import li.songe.gkd.db.DbSet @@ -70,7 +71,6 @@ import li.songe.gkd.ui.style.titleItemPadding import li.songe.gkd.util.LOCAL_SUBS_ID import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.ProfileTransitions -import li.songe.gkd.util.ResolvedGroup import li.songe.gkd.util.RuleSortOption import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.launchTry diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AppConfigVm.kt b/app/src/main/kotlin/li/songe/gkd/ui/AppConfigVm.kt index 7b9c13c..86d4312 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/AppConfigVm.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/AppConfigVm.kt @@ -9,10 +9,10 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import li.songe.gkd.data.ResolvedAppGroup +import li.songe.gkd.data.ResolvedGlobalGroup import li.songe.gkd.data.SubsConfig import li.songe.gkd.db.DbSet -import li.songe.gkd.util.ResolvedAppGroup -import li.songe.gkd.util.ResolvedGlobalGroup import li.songe.gkd.util.RuleSortOption import li.songe.gkd.util.collator import li.songe.gkd.util.findOption diff --git a/app/src/main/kotlin/li/songe/gkd/util/Constants.kt b/app/src/main/kotlin/li/songe/gkd/util/Constants.kt index ff1db3d..c955ce4 100644 --- a/app/src/main/kotlin/li/songe/gkd/util/Constants.kt +++ b/app/src/main/kotlin/li/songe/gkd/util/Constants.kt @@ -6,7 +6,7 @@ const val FILE_SHORT_URL = "https://f.gkd.li/" const val IMPORT_SHORT_URL = "https://i.gkd.li/i/" const val SERVER_SCRIPT_URL = - "https://registry-direct.npmmirror.com/@gkd-kit/config/latest/files/dist/server.js" + "https://registry.npmmirror.com/@gkd-kit/config/latest/files/dist/server.js" const val REPOSITORY_URL = "https://github.com/gkd-kit/gkd" diff --git a/app/src/main/kotlin/li/songe/gkd/util/CoroutineExt.kt b/app/src/main/kotlin/li/songe/gkd/util/CoroutineExt.kt index f8cfc3a..818398b 100644 --- a/app/src/main/kotlin/li/songe/gkd/util/CoroutineExt.kt +++ b/app/src/main/kotlin/li/songe/gkd/util/CoroutineExt.kt @@ -18,6 +18,7 @@ fun CoroutineScope.launchTry( block() } catch (e: CancellationException) { e.printStackTrace() + } catch (_: InterruptRuleMatchException) { } catch (e: Exception) { e.printStackTrace() LogUtils.d(e) diff --git a/app/src/main/kotlin/li/songe/gkd/util/Others.kt b/app/src/main/kotlin/li/songe/gkd/util/Others.kt index 4fe342b..b522e8d 100644 --- a/app/src/main/kotlin/li/songe/gkd/util/Others.kt +++ b/app/src/main/kotlin/li/songe/gkd/util/Others.kt @@ -25,4 +25,6 @@ fun Bitmap.isEmptyBitmap(): Boolean { } } return true -} \ No newline at end of file +} + +class InterruptRuleMatchException() : Exception() \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt b/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt index f6e49d4..3204b0f 100644 --- a/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt +++ b/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt @@ -22,6 +22,8 @@ import li.songe.gkd.data.AppRule import li.songe.gkd.data.CategoryConfig import li.songe.gkd.data.GlobalRule import li.songe.gkd.data.RawSubscription +import li.songe.gkd.data.ResolvedAppGroup +import li.songe.gkd.data.ResolvedGlobalGroup import li.songe.gkd.data.SubsConfig import li.songe.gkd.data.SubsItem import li.songe.gkd.data.SubsVersion diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7eda136..c015a10 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.0.21" -ksp = "2.0.21-RC-1.0.25" -android = "8.7.0" +ksp = "2.0.21-1.0.25" +android = "8.7.1" compose = "1.7.3" rikka = "4.4.0" room = "2.6.1" diff --git a/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt b/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt index 92901ac..3c40352 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt @@ -69,21 +69,6 @@ class Selector( expressions.distinct().toTypedArray() } - val useCache = run { - if (connectWrappers.isNotEmpty()) { - return@run true - } - binaryExpressions.forEach { b -> - if (b.properties.any { useCacheProperties.contains(it) }) { - return@run true - } - if (b.methods.any { useCacheMethods.contains(it) }) { - return@run true - } - } - return@run false - } - fun isSlow(matchOption: MatchOption): Boolean { if (matchOption.quickFind && quickFindValue == null && !isMatchRoot) { return true @@ -119,19 +104,12 @@ class Selector( fun parse(source: String) = selectorParser(source) fun parseOrNull(source: String) = try { selectorParser(source) - } catch (e: Exception) { + } catch (_: Exception) { null } } } -private val useCacheProperties by lazy { - arrayOf("index", "parent", "depth") -} -private val useCacheMethods by lazy { - arrayOf("getChild") -} - private fun getExpType(exp: ValueExpression, typeInfo: TypeInfo): PrimitiveType? { return when (exp) { is ValueExpression.NullLiteral -> null diff --git a/selector/src/commonMain/kotlin/li/songe/selector/Transform.kt b/selector/src/commonMain/kotlin/li/songe/selector/Transform.kt index 1a33e88..21fe838 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/Transform.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/Transform.kt @@ -2,6 +2,7 @@ package li.songe.selector import kotlin.js.JsExport +@Suppress("unused") @JsExport class Transform @JsExport.Ignore constructor( val getAttr: (Any, String) -> Any?, @@ -13,7 +14,7 @@ class Transform @JsExport.Ignore constructor( val getRoot: (T) -> T? = { node -> var parentVar: T? = getParent(node) while (parentVar != null) { - parentVar = getParent(parentVar!!) + parentVar = getParent(parentVar) } parentVar }, @@ -59,7 +60,7 @@ class Transform @JsExport.Ignore constructor( var parentVar: T? = getParent(node) ?: return@sequence var offset = 0 while (parentVar != null) { - parentVar?.let { + parentVar.let { if (connectExpression.checkOffset(offset)) { yield(it) }