feat: priority rule
Some checks are pending
Build-Apk / build (push) Waiting to run

This commit is contained in:
lisonge 2024-10-16 22:46:19 +08:00
parent 4fc031a3fe
commit 1825b000c0
23 changed files with 925 additions and 997 deletions

View File

@ -208,11 +208,11 @@ private fun Activity.fixTopPadding() {
@Composable @Composable
private fun ShizukuErrorDialog(stateFlow: MutableStateFlow<Boolean>) { private fun ShizukuErrorDialog(stateFlow: MutableStateFlow<Boolean>) {
val state = stateFlow.collectAsState() val state = stateFlow.collectAsState().value
if (state.value) { if (state) {
val appId = "moe.shizuku.privileged.api" val appId = "moe.shizuku.privileged.api"
val appInfoCache = appInfoCacheFlow.collectAsState() val appInfoCache = appInfoCacheFlow.collectAsState().value
val installed = appInfoCache.value.contains(appId) val installed = appInfoCache.contains(appId)
AlertDialog( AlertDialog(
onDismissRequest = { stateFlow.value = false }, onDismissRequest = { stateFlow.value = false },
title = { Text(text = "授权错误") }, title = { Text(text = "授权错误") },

View File

@ -1,7 +1,5 @@
package li.songe.gkd.data package li.songe.gkd.data
import li.songe.gkd.util.ResolvedAppGroup
class AppRule( class AppRule(
rule: RawSubscription.RawAppRule, rule: RawSubscription.RawAppRule,
g: ResolvedAppGroup, g: ResolvedAppGroup,

View File

@ -48,7 +48,7 @@ fun createComplexSnapshot(): ComplexSnapshot {
screenWidth = ScreenUtils.getScreenWidth(), screenWidth = ScreenUtils.getScreenWidth(),
isLandscape = ScreenUtils.isLandscape(), isLandscape = ScreenUtils.isLandscape(),
nodes = NodeInfo.info2nodeList(currentAbNode) nodes = info2nodeList(currentAbNode)
) )
} }

View File

@ -1,7 +1,6 @@
package li.songe.gkd.data package li.songe.gkd.data
import li.songe.gkd.service.launcherAppId import li.songe.gkd.service.launcherAppId
import li.songe.gkd.util.ResolvedGlobalGroup
import li.songe.gkd.util.systemAppsFlow import li.songe.gkd.util.systemAppsFlow
data class GlobalApp( data class GlobalApp(

View File

@ -3,7 +3,7 @@ package li.songe.gkd.data
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import kotlinx.serialization.Serializable 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.service.topActivityFlow
import li.songe.gkd.util.toast import li.songe.gkd.util.toast
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@ -15,20 +15,14 @@ data class NodeInfo(
val idQf: Boolean?, val idQf: Boolean?,
val textQf: Boolean?, val textQf: Boolean?,
val attr: AttrInfo, val attr: AttrInfo,
) { )
companion object { private data class TempNodeData(
private const val MAX_KEEP_SIZE = 5000
// 先获取所有节点构建树结构, 然后再判断 idQf/textQf 如果存在一个能同时 idQf 和 textQf 的节点, 则认为 idQf 和 textQf 等价
data class TempNodeData(
val node: AccessibilityNodeInfo, val node: AccessibilityNodeInfo,
val parent: TempNodeData?, val parent: TempNodeData?,
val index: Int, val index: Int,
val depth: Int, val depth: Int,
) { ) {
var id = 0 var id = 0
val attr = AttrInfo.info2data(node, index, depth) val attr = AttrInfo.info2data(node, index, depth)
var children: List<TempNodeData> = emptyList() var children: List<TempNodeData> = emptyList()
@ -45,9 +39,19 @@ data class NodeInfo(
field = value field = value
textQfInit = true textQfInit = true
} }
} }
fun info2nodeList(root: AccessibilityNodeInfo?): List<NodeInfo> { 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
// 先获取所有节点构建树结构, 然后再判断 idQf/textQf 如果存在一个能同时 idQf 和 textQf 的节点, 则认为 idQf 和 textQf 等价
fun info2nodeList(root: AccessibilityNodeInfo?): List<NodeInfo> {
if (root == null) { if (root == null) {
return emptyList() return emptyList()
} }
@ -212,6 +216,5 @@ data class NodeInfo(
attr = n.attr attr = n.attr
) )
} }
}
}
} }

View File

@ -190,6 +190,8 @@ data class RawSubscription(
val snapshotUrls: List<String>? val snapshotUrls: List<String>?
val excludeSnapshotUrls: List<String>? val excludeSnapshotUrls: List<String>?
val exampleUrls: List<String>? val exampleUrls: List<String>?
val priorityTime: Long?
val priorityActionMaximum: Int?
} }
sealed interface RawRuleProps : RawCommonProps { sealed interface RawRuleProps : RawCommonProps {
@ -267,6 +269,8 @@ data class RawSubscription(
override val resetMatch: String?, override val resetMatch: String?,
override val actionCdKey: Int?, override val actionCdKey: Int?,
override val actionMaximumKey: Int?, override val actionMaximumKey: Int?,
override val priorityTime: Long?,
override val priorityActionMaximum: Int?,
override val order: Int?, override val order: Int?,
override val forcedTime: Long?, override val forcedTime: Long?,
override val snapshotUrls: List<String>?, override val snapshotUrls: List<String>?,
@ -306,6 +310,8 @@ data class RawSubscription(
override val resetMatch: String?, override val resetMatch: String?,
override val actionCdKey: Int?, override val actionCdKey: Int?,
override val actionMaximumKey: Int?, override val actionMaximumKey: Int?,
override val priorityTime: Long?,
override val priorityActionMaximum: Int?,
override val order: Int?, override val order: Int?,
override val forcedTime: Long?, override val forcedTime: Long?,
override val snapshotUrls: List<String>?, override val snapshotUrls: List<String>?,
@ -340,6 +346,8 @@ data class RawSubscription(
override val fastQuery: Boolean?, override val fastQuery: Boolean?,
override val matchRoot: Boolean?, override val matchRoot: Boolean?,
override val actionMaximum: Int?, override val actionMaximum: Int?,
override val priorityTime: Long?,
override val priorityActionMaximum: Int?,
override val order: Int?, override val order: Int?,
override val forcedTime: Long?, override val forcedTime: Long?,
override val matchDelay: Long?, override val matchDelay: Long?,
@ -385,6 +393,8 @@ data class RawSubscription(
override val fastQuery: Boolean?, override val fastQuery: Boolean?,
override val matchRoot: Boolean?, override val matchRoot: Boolean?,
override val actionMaximum: Int?, override val actionMaximum: Int?,
override val priorityTime: Long?,
override val priorityActionMaximum: Int?,
override val order: Int?, override val order: Int?,
override val forcedTime: Long?, override val forcedTime: Long?,
override val matchDelay: Long?, override val matchDelay: Long?,
@ -626,6 +636,8 @@ data class RawSubscription(
excludeVersionNames = getStringIArray(jsonObject, "excludeVersionNames"), excludeVersionNames = getStringIArray(jsonObject, "excludeVersionNames"),
position = getPosition(jsonObject), position = getPosition(jsonObject),
forcedTime = getLong(jsonObject, "forcedTime"), forcedTime = getLong(jsonObject, "forcedTime"),
priorityTime = getLong(jsonObject, "priorityTime"),
priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"),
) )
} }
@ -671,6 +683,8 @@ data class RawSubscription(
excludeVersionCodes = getLongIArray(jsonObject, "excludeVersionCodes"), excludeVersionCodes = getLongIArray(jsonObject, "excludeVersionCodes"),
versionNames = getStringIArray(jsonObject, "versionNames"), versionNames = getStringIArray(jsonObject, "versionNames"),
excludeVersionNames = getStringIArray(jsonObject, "excludeVersionNames"), excludeVersionNames = getStringIArray(jsonObject, "excludeVersionNames"),
priorityTime = getLong(jsonObject, "priorityTime"),
priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"),
) )
} }
@ -736,6 +750,8 @@ data class RawSubscription(
order = getInt(jsonObject, "order"), order = getInt(jsonObject, "order"),
forcedTime = getLong(jsonObject, "forcedTime"), forcedTime = getLong(jsonObject, "forcedTime"),
position = getPosition(jsonObject), position = getPosition(jsonObject),
priorityTime = getLong(jsonObject, "priorityTime"),
priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"),
) )
} }
@ -773,6 +789,8 @@ data class RawSubscription(
order = getInt(jsonObject, "order"), order = getInt(jsonObject, "order"),
scopeKeys = getIntIArray(jsonObject, "scopeKeys"), scopeKeys = getIntIArray(jsonObject, "scopeKeys"),
forcedTime = getLong(jsonObject, "forcedTime"), forcedTime = getLong(jsonObject, "forcedTime"),
priorityTime = getLong(jsonObject, "priorityTime"),
priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"),
) )
} }

View File

@ -1,15 +1,13 @@
package li.songe.gkd.util package li.songe.gkd.data
import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.SubsConfig
import li.songe.gkd.data.SubsItem
sealed class ResolvedGroup( sealed class ResolvedGroup(
open val group: RawSubscription.RawGroupProps, open val group: RawSubscription.RawGroupProps,
val subscription: RawSubscription, val subscription: RawSubscription,
val subsItem: SubsItem, val subsItem: SubsItem,
val config: SubsConfig?, val config: SubsConfig?,
) ) {
val excludeData = ExcludeData.parse(config?.exclude)
}
class ResolvedAppGroup( class ResolvedAppGroup(
override val group: RawSubscription.RawAppGroup, override val group: RawSubscription.RawAppGroup,

View File

@ -1,18 +1,10 @@
package li.songe.gkd.data package li.songe.gkd.data
import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService
import android.util.Log
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.coroutines.Job 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.lastTriggerRule
import li.songe.gkd.service.lastTriggerTime 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.MatchOption
import li.songe.selector.Selector import li.songe.selector.Selector
@ -26,12 +18,13 @@ sealed class ResolvedRule(
val config = g.config val config = g.config
val key = rule.key val key = rule.key
val index = group.rules.indexOfFirst { r -> r === rule } val index = group.rules.indexOfFirst { r -> r === rule }
val excludeData = g.excludeData
private val preKeys = (rule.preKeys ?: emptyList()).toSet() private val preKeys = (rule.preKeys ?: emptyList()).toSet()
private val matches = val matches =
(rule.matches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) } (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) } (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) } (rule.anyMatches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) }
private val resetMatch = rule.resetMatch ?: group.resetMatch private val resetMatch = rule.resetMatch ?: group.resetMatch
@ -39,11 +32,11 @@ sealed class ResolvedRule(
val actionDelay = rule.actionDelay ?: group.actionDelay ?: 0L val actionDelay = rule.actionDelay ?: group.actionDelay ?: 0L
private val matchTime = rule.matchTime ?: group.matchTime private val matchTime = rule.matchTime ?: group.matchTime
private val forcedTime = rule.forcedTime ?: group.forcedTime ?: 0L private val forcedTime = rule.forcedTime ?: group.forcedTime ?: 0L
private val matchOption = MatchOption( val matchOption = MatchOption(
quickFind = rule.quickFind ?: group.quickFind ?: false, quickFind = rule.quickFind ?: group.quickFind ?: false,
fastQuery = rule.fastQuery ?: group.fastQuery ?: 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 val order = rule.order ?: group.order ?: 0
private val actionCdKey = rule.actionCdKey ?: group.actionCdKey private val actionCdKey = rule.actionCdKey ?: group.actionCdKey
@ -63,6 +56,18 @@ sealed class ResolvedRule(
private val hasSlowSelector by lazy { private val hasSlowSelector by lazy {
(matches + excludeMatches + anyMatches).any { s -> s.isSlow(matchOption) } (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 } val isSlow by lazy { preKeys.isEmpty() && (matchTime == null || matchTime > 10_000L) && hasSlowSelector }
@ -138,52 +143,6 @@ sealed class ResolvedRule(
else -> true 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 { private val performer = ActionPerformer.getAction(rule.action ?: rule.position?.let {
ActionPerformer.ClickCenter.action 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}" 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 val type: String
// 范围越精确, 优先级越高 // 范围越精确, 优先级越高
abstract fun matchActivity(appId: String, activityId: String? = null): Boolean abstract fun matchActivity(appId: String, activityId: String? = null): Boolean
} }
sealed class RuleStatus(val name: String) { sealed class RuleStatus(val name: String) {
@ -246,13 +202,17 @@ sealed class RuleStatus(val name: String) {
data object Status4 : RuleStatus("超出匹配时间") data object Status4 : RuleStatus("超出匹配时间")
data object Status5 : RuleStatus("处于冷却时间") data object Status5 : RuleStatus("处于冷却时间")
data object Status6 : RuleStatus("处于点击延迟") data object Status6 : RuleStatus("处于点击延迟")
val ok: Boolean
get() = this === StatusOk
} }
fun getFixActivityIds( fun getFixActivityIds(
appId: String, appId: String,
activityIds: List<String>?, activityIds: List<String>?,
): List<String> { ): List<String> {
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 if (activityId.startsWith('.')) { // .a.b.c -> com.x.y.x.a.b.c
appId + activityId appId + activityId
} else { } 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()
}

View File

@ -11,7 +11,7 @@ import li.songe.gkd.debug.SnapshotExt.captureSnapshot
import li.songe.gkd.service.A11yService import li.songe.gkd.service.A11yService
import li.songe.gkd.service.TopActivity import li.songe.gkd.service.TopActivity
import li.songe.gkd.service.getAndUpdateCurrentRules 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.service.updateTopActivity
import li.songe.gkd.shizuku.safeGetTopActivity import li.songe.gkd.shizuku.safeGetTopActivity
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry
@ -27,7 +27,7 @@ class SnapshotTileService : TileService() {
return return
} }
appScope.launchTry(Dispatchers.IO) { appScope.launchTry(Dispatchers.IO) {
val oldAppId = service.safeActiveWindow?.packageName?.toString() val oldAppId = service.safeActiveWindowAppId
?: return@launchTry toast("获取界面信息根节点失败") ?: return@launchTry toast("获取界面信息根节点失败")
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
@ -37,7 +37,7 @@ class SnapshotTileService : TileService() {
val timeoutText = "没有检测到界面切换,捕获失败" val timeoutText = "没有检测到界面切换,捕获失败"
while (true) { while (true) {
val latestAppId = service.safeActiveWindow?.packageName?.toString() val latestAppId = service.safeActiveWindowAppId
if (latestAppId == null) { if (latestAppId == null) {
// https://github.com/gkd-kit/gkd/issues/713 // https://github.com/gkd-kit/gkd/issues/713
delay(250) delay(250)

View File

@ -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 <K, V> LruCache<K, V>.set(child: K, value: V): V {
return put(child, value)
}
private fun List<Any>.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<Pair<AccessibilityNodeInfo, Int>, AccessibilityNodeInfo>(MAX_CACHE_SIZE)
private var indexCache = LruCache<AccessibilityNodeInfo, Int>(MAX_CACHE_SIZE)
private var parentCache = LruCache<AccessibilityNodeInfo, AccessibilityNodeInfo>(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<AccessibilityNodeInfo> {
guardInterrupt()
return node.findAccessibilityNodeInfosByText(value)
}
private fun getA11ById(
node: AccessibilityNodeInfo,
value: String
): List<AccessibilityNodeInfo> {
guardInterrupt()
return node.findAccessibilityNodeInfosByViewId(value)
}
private fun getFastQueryNodes(
node: AccessibilityNodeInfo,
fastQuery: FastQuery
): List<AccessibilityNodeInfo> {
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<AccessibilityNodeInfo> {
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<AccessibilityNodeInfo>()
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<AccessibilityNodeInfo>()
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()

View File

@ -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<AccessibilityNodeInfo>,
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<AccessibilityNodeInfo> {
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<FastQuery>
): Sequence<AccessibilityNodeInfo> {
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<AccessibilityNodeInfo> = { 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<AccessibilityNodeInfo>,
val cache: NodeCache,
)
private operator fun <K, V> LruCache<K, V>.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<Pair<AccessibilityNodeInfo, Int>, AccessibilityNodeInfo>(MAX_CACHE_SIZE)
private var indexMap = LruCache<AccessibilityNodeInfo, Int>(MAX_CACHE_SIZE)
private var parentMap = LruCache<AccessibilityNodeInfo, AccessibilityNodeInfo>(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<Int>
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<AccessibilityNodeInfo> = { 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<AccessibilityNodeInfo>()
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<AccessibilityNodeInfo>()
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<Any>.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<AccessibilityNodeInfo>()
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)
}

View File

@ -41,7 +41,6 @@ import li.songe.gkd.data.GkdAction
import li.songe.gkd.data.ResolvedRule import li.songe.gkd.data.ResolvedRule
import li.songe.gkd.data.RpcError import li.songe.gkd.data.RpcError
import li.songe.gkd.data.RuleStatus import li.songe.gkd.data.RuleStatus
import li.songe.gkd.data.clearNodeCache
import li.songe.gkd.debug.SnapshotExt import li.songe.gkd.debug.SnapshotExt
import li.songe.gkd.permission.shizukuOkState import li.songe.gkd.permission.shizukuOkState
import li.songe.gkd.shizuku.safeGetTopActivity import li.songe.gkd.shizuku.safeGetTopActivity
@ -85,10 +84,9 @@ class A11yService : AccessibilityService(), OnCreate, OnA11yConnected, OnA11yEve
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
onDestroyed() onDestroyed()
scope.cancel()
} }
val scope = CoroutineScope(Dispatchers.Default) val scope = CoroutineScope(Dispatchers.Default).apply { onDestroyed { cancel() } }
init { init {
useRunningState() useRunningState()
@ -119,12 +117,20 @@ class A11yService : AccessibilityService(), OnCreate, OnA11yConnected, OnA11yEve
selector.checkSelector()?.let { selector.checkSelector()?.let {
throw RpcError(it) throw RpcError(it)
} }
val targetNode = serviceVal.safeActiveWindow?.querySelector( val matchOption = MatchOption(
selector, MatchOption(
quickFind = gkdAction.quickFind, quickFind = gkdAction.quickFind,
fastQuery = gkdAction.fastQuery, fastQuery = gkdAction.fastQuery,
), createCacheTransform().transform, isRootNode = true )
) ?: throw RpcError("没有查询到节点") val cache = A11yContext(true)
val targetNode = serviceVal.safeActiveWindow?.let {
cache.rootCache = it
cache.querySelector(
it,
selector,
matchOption
)
} ?: throw RpcError("没有查询到节点")
if (gkdAction.action == null) { if (gkdAction.action == null) {
// 仅查询 // 仅查询
@ -198,18 +204,19 @@ private fun A11yService.useMatchRule() {
} }
null null
} else { } else {
if (META.debuggable) {
Log.d(
"queryEvents",
"保留最后两个事件:${queryEvents.first().appId}${queryEvents.map { it.className }}"
)
}
// type,appId,className 一致, 需要在 synchronized 外验证是否是同一节点 // type,appId,className 一致, 需要在 synchronized 外验证是否是同一节点
arrayOf( arrayOf(
queryEvents[queryEvents.size - 2], queryEvents[queryEvents.size - 2],
queryEvents.last(), queryEvents.last(),
).apply {
if (META.debuggable) {
Log.d(
"queryEvents",
"保留最后两个事件:${queryEvents.first().appId}=${map { it.className }}"
) )
} }
}
}
} else if (queryEvents.size == 1) { } else if (queryEvents.size == 1) {
if (META.debuggable) { if (META.debuggable) {
Log.d( Log.d(
@ -246,8 +253,7 @@ private fun A11yService.useMatchRule() {
} }
} }
var lastNodeUsed = false var lastNodeUsed = false
clearNodeCache() for (rule in activityRule.priorityRules) { // 规则数量有可能过多导致耗时过长
for (rule in activityRule.currentRules) { // 规则数量有可能过多导致耗时过长
if (delayRule != null && delayRule !== rule) continue if (delayRule != null && delayRule !== rule) continue
val statusCode = rule.status val statusCode = rule.status
if (statusCode == RuleStatus.Status3 && rule.matchDelayJob == null) { if (statusCode == RuleStatus.Status3 && rule.matchDelayJob == null) {
@ -299,7 +305,7 @@ private fun A11yService.useMatchRule() {
return@launchQuery return@launchQuery
} }
if (!matchApp) continue if (!matchApp) continue
val target = rule.query(nodeVal, lastNode == null) ?: continue val target = a11yContext.queryRule(rule, nodeVal) ?: continue
if (activityRule !== getAndUpdateCurrentRules()) break if (activityRule !== getAndUpdateCurrentRules()) break
if (rule.checkDelay() && rule.actionDelayJob == null) { if (rule.checkDelay() && rule.actionDelayJob == null) {
rule.actionDelayJob = scope.launch(A11yService.actionThread) { rule.actionDelayJob = scope.launch(A11yService.actionThread) {
@ -364,10 +370,10 @@ private fun A11yService.useMatchRule() {
// https://github.com/gkd-kit/gkd/issues/622 // https://github.com/gkd-kit/gkd/issues/622
lastGetAppIdTime = t lastGetAppIdTime = t
lastAppId = if (storeFlow.value.enableShizukuActivity) { lastAppId = if (storeFlow.value.enableShizukuActivity) {
withTimeoutOrNull(100) { activeWindowAppId } ?: safeGetTopActivity()?.appId withTimeoutOrNull(100) { safeActiveWindowAppId } ?: safeGetTopActivity()?.appId
} else { } else {
null null
} ?: activeWindowAppId } ?: safeActiveWindowAppId
} }
return lastAppId return lastAppId
} }
@ -438,6 +444,7 @@ private fun A11yService.useMatchRule() {
} }
synchronized(queryEvents) { queryEvents.addAll(consumedEvents) } synchronized(queryEvents) { queryEvents.addAll(consumedEvents) }
a11yContext.interruptKey++
newQueryTask(a11yEvent) newQueryTask(a11yEvent)
} }

View File

@ -31,6 +31,10 @@ data class TopActivity(
fun format(): String { fun format(): String {
return "${appId}/${activityId}/${number}" return "${appId}/${activityId}/${number}"
} }
fun sameAs(other: TopActivity): Boolean {
return appId == other.appId && activityId == other.activityId
}
} }
val topActivityFlow = MutableStateFlow(TopActivity()) val topActivityFlow = MutableStateFlow(TopActivity())
@ -39,17 +43,16 @@ private val activityLogMutex by lazy { Mutex() }
private var activityLogCount = 0 private var activityLogCount = 0
private var lastActivityChangeTime = 0L private var lastActivityChangeTime = 0L
fun updateTopActivity(topActivity: TopActivity) { fun updateTopActivity(topActivity: TopActivity) {
val isSameActivity = val isSameActivity = topActivityFlow.value.sameAs(topActivity)
topActivityFlow.value.appId == topActivity.appId && topActivityFlow.value.activityId == topActivity.activityId
if (isSameActivity) { if (isSameActivity) {
if (isActivityVisible() && topActivity.appId == META.appId) {
return
}
if (topActivityFlow.value.number == topActivity.number) { if (topActivityFlow.value.number == topActivity.number) {
return return
} }
if (isActivityVisible() && topActivity.appId == META.appId) {
return
}
val t = System.currentTimeMillis() val t = System.currentTimeMillis()
if (t - lastActivityChangeTime < 1000) { if (t - lastActivityChangeTime < 1500) {
return return
} }
} }
@ -76,13 +79,22 @@ fun updateTopActivity(topActivity: TopActivity) {
lastActivityChangeTime = System.currentTimeMillis() lastActivityChangeTime = System.currentTimeMillis()
} }
data class ActivityRule( class ActivityRule(
val appRules: List<AppRule> = emptyList(), val appRules: List<AppRule> = emptyList(),
val globalRules: List<GlobalRule> = emptyList(), val globalRules: List<GlobalRule> = emptyList(),
val topActivity: TopActivity = TopActivity(), val topActivity: TopActivity = TopActivity(),
val ruleSummary: RuleSummary = RuleSummary(), 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<ResolvedRule>
get() = if (hasPriorityRule) {
currentRules.sortedBy { if (it.isPriority()) 0 else 1 }
} else {
currentRules
}
} }
val activityRuleFlow by lazy { MutableStateFlow(ActivityRule()) } val activityRuleFlow by lazy { MutableStateFlow(ActivityRule()) }

View File

@ -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()}"
}
}

View File

@ -57,6 +57,7 @@ import kotlinx.coroutines.flow.update
import li.songe.gkd.MainActivity import li.songe.gkd.MainActivity
import li.songe.gkd.data.ExcludeData import li.songe.gkd.data.ExcludeData
import li.songe.gkd.data.RawSubscription import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.ResolvedGroup
import li.songe.gkd.data.SubsConfig import li.songe.gkd.data.SubsConfig
import li.songe.gkd.data.stringify import li.songe.gkd.data.stringify
import li.songe.gkd.db.DbSet 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.LOCAL_SUBS_ID
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.ResolvedGroup
import li.songe.gkd.util.RuleSortOption import li.songe.gkd.util.RuleSortOption
import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.appInfoCacheFlow
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry

View File

@ -9,10 +9,10 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn 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.data.SubsConfig
import li.songe.gkd.db.DbSet 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.RuleSortOption
import li.songe.gkd.util.collator import li.songe.gkd.util.collator
import li.songe.gkd.util.findOption import li.songe.gkd.util.findOption

View File

@ -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 IMPORT_SHORT_URL = "https://i.gkd.li/i/"
const val SERVER_SCRIPT_URL = 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" const val REPOSITORY_URL = "https://github.com/gkd-kit/gkd"

View File

@ -18,6 +18,7 @@ fun CoroutineScope.launchTry(
block() block()
} catch (e: CancellationException) { } catch (e: CancellationException) {
e.printStackTrace() e.printStackTrace()
} catch (_: InterruptRuleMatchException) {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
LogUtils.d(e) LogUtils.d(e)

View File

@ -26,3 +26,5 @@ fun Bitmap.isEmptyBitmap(): Boolean {
} }
return true return true
} }
class InterruptRuleMatchException() : Exception()

View File

@ -22,6 +22,8 @@ import li.songe.gkd.data.AppRule
import li.songe.gkd.data.CategoryConfig import li.songe.gkd.data.CategoryConfig
import li.songe.gkd.data.GlobalRule import li.songe.gkd.data.GlobalRule
import li.songe.gkd.data.RawSubscription 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.SubsConfig
import li.songe.gkd.data.SubsItem import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.SubsVersion import li.songe.gkd.data.SubsVersion

View File

@ -1,7 +1,7 @@
[versions] [versions]
kotlin = "2.0.21" kotlin = "2.0.21"
ksp = "2.0.21-RC-1.0.25" ksp = "2.0.21-1.0.25"
android = "8.7.0" android = "8.7.1"
compose = "1.7.3" compose = "1.7.3"
rikka = "4.4.0" rikka = "4.4.0"
room = "2.6.1" room = "2.6.1"

View File

@ -69,21 +69,6 @@ class Selector(
expressions.distinct().toTypedArray() 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 { fun isSlow(matchOption: MatchOption): Boolean {
if (matchOption.quickFind && quickFindValue == null && !isMatchRoot) { if (matchOption.quickFind && quickFindValue == null && !isMatchRoot) {
return true return true
@ -119,19 +104,12 @@ class Selector(
fun parse(source: String) = selectorParser(source) fun parse(source: String) = selectorParser(source)
fun parseOrNull(source: String) = try { fun parseOrNull(source: String) = try {
selectorParser(source) selectorParser(source)
} catch (e: Exception) { } catch (_: Exception) {
null 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? { private fun getExpType(exp: ValueExpression, typeInfo: TypeInfo): PrimitiveType? {
return when (exp) { return when (exp) {
is ValueExpression.NullLiteral -> null is ValueExpression.NullLiteral -> null

View File

@ -2,6 +2,7 @@ package li.songe.selector
import kotlin.js.JsExport import kotlin.js.JsExport
@Suppress("unused")
@JsExport @JsExport
class Transform<T> @JsExport.Ignore constructor( class Transform<T> @JsExport.Ignore constructor(
val getAttr: (Any, String) -> Any?, val getAttr: (Any, String) -> Any?,
@ -13,7 +14,7 @@ class Transform<T> @JsExport.Ignore constructor(
val getRoot: (T) -> T? = { node -> val getRoot: (T) -> T? = { node ->
var parentVar: T? = getParent(node) var parentVar: T? = getParent(node)
while (parentVar != null) { while (parentVar != null) {
parentVar = getParent(parentVar!!) parentVar = getParent(parentVar)
} }
parentVar parentVar
}, },
@ -59,7 +60,7 @@ class Transform<T> @JsExport.Ignore constructor(
var parentVar: T? = getParent(node) ?: return@sequence var parentVar: T? = getParent(node) ?: return@sequence
var offset = 0 var offset = 0
while (parentVar != null) { while (parentVar != null) {
parentVar?.let { parentVar.let {
if (connectExpression.checkOffset(offset)) { if (connectExpression.checkOffset(offset)) {
yield(it) yield(it)
} }