mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-15 19:22:26 +08:00
This commit is contained in:
parent
4fc031a3fe
commit
1825b000c0
|
@ -208,11 +208,11 @@ private fun Activity.fixTopPadding() {
|
|||
|
||||
@Composable
|
||||
private fun ShizukuErrorDialog(stateFlow: MutableStateFlow<Boolean>) {
|
||||
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 = "授权错误") },
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import li.songe.gkd.util.ResolvedAppGroup
|
||||
|
||||
class AppRule(
|
||||
rule: RawSubscription.RawAppRule,
|
||||
g: ResolvedAppGroup,
|
||||
|
|
|
@ -48,7 +48,7 @@ fun createComplexSnapshot(): ComplexSnapshot {
|
|||
screenWidth = ScreenUtils.getScreenWidth(),
|
||||
isLandscape = ScreenUtils.isLandscape(),
|
||||
|
||||
nodes = NodeInfo.info2nodeList(currentAbNode)
|
||||
nodes = info2nodeList(currentAbNode)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<TempNodeData> = 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<TempNodeData> = 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<NodeInfo> {
|
||||
if (root == null) {
|
||||
return emptyList()
|
||||
}
|
||||
val nodes = mutableListOf<TempNodeData>()
|
||||
val collectTime = measureTimeMillis {
|
||||
val stack = mutableListOf<TempNodeData>()
|
||||
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<String, List<AccessibilityNodeInfo>>()
|
||||
val textQfCache = mutableMapOf<String, List<AccessibilityNodeInfo>>()
|
||||
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<NodeInfo> {
|
||||
if (root == null) {
|
||||
return emptyList()
|
||||
}
|
||||
val nodes = mutableListOf<TempNodeData>()
|
||||
val collectTime = measureTimeMillis {
|
||||
val stack = mutableListOf<TempNodeData>()
|
||||
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<String, List<AccessibilityNodeInfo>>()
|
||||
val textQfCache = mutableMapOf<String, List<AccessibilityNodeInfo>>()
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -190,6 +190,8 @@ data class RawSubscription(
|
|||
val snapshotUrls: List<String>?
|
||||
val excludeSnapshotUrls: List<String>?
|
||||
val exampleUrls: List<String>?
|
||||
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<String>?,
|
||||
|
@ -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<String>?,
|
||||
|
@ -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"),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
|
@ -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<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
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
526
app/src/main/kotlin/li/songe/gkd/service/A11yContext.kt
Normal file
526
app/src/main/kotlin/li/songe/gkd/service/A11yContext.kt
Normal 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()
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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<AppRule> = emptyList(),
|
||||
val globalRules: List<GlobalRule> = 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<ResolvedRule>
|
||||
get() = if (hasPriorityRule) {
|
||||
currentRules.sortedBy { if (it.isPriority()) 0 else 1 }
|
||||
} else {
|
||||
currentRules
|
||||
}
|
||||
}
|
||||
|
||||
val activityRuleFlow by lazy { MutableStateFlow(ActivityRule()) }
|
||||
|
|
89
app/src/main/kotlin/li/songe/gkd/service/NodeExt.kt
Normal file
89
app/src/main/kotlin/li/songe/gkd/service/NodeExt.kt
Normal 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()}"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ fun CoroutineScope.launchTry(
|
|||
block()
|
||||
} catch (e: CancellationException) {
|
||||
e.printStackTrace()
|
||||
} catch (_: InterruptRuleMatchException) {
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
LogUtils.d(e)
|
||||
|
|
|
@ -26,3 +26,5 @@ fun Bitmap.isEmptyBitmap(): Boolean {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class InterruptRuleMatchException() : Exception()
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,6 +2,7 @@ package li.songe.selector
|
|||
|
||||
import kotlin.js.JsExport
|
||||
|
||||
@Suppress("unused")
|
||||
@JsExport
|
||||
class Transform<T> @JsExport.Ignore constructor(
|
||||
val getAttr: (Any, String) -> Any?,
|
||||
|
@ -13,7 +14,7 @@ class Transform<T> @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<T> @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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user