mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 03:32:38 +08:00
This commit is contained in:
parent
4fc031a3fe
commit
1825b000c0
|
@ -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 = "授权错误") },
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,203 +15,206 @@ data class NodeInfo(
|
||||||
val idQf: Boolean?,
|
val idQf: Boolean?,
|
||||||
val textQf: Boolean?,
|
val textQf: Boolean?,
|
||||||
val attr: AttrInfo,
|
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 {
|
var idQfInit = false
|
||||||
|
var idQf: Boolean? = null
|
||||||
private const val MAX_KEEP_SIZE = 5000
|
set(value) {
|
||||||
|
field = value
|
||||||
// 先获取所有节点构建树结构, 然后再判断 idQf/textQf 如果存在一个能同时 idQf 和 textQf 的节点, 则认为 idQf 和 textQf 等价
|
idQfInit = true
|
||||||
|
|
||||||
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 textQfInit = false
|
||||||
|
var textQf: Boolean? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
textQfInit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun info2nodeList(root: AccessibilityNodeInfo?): List<NodeInfo> {
|
private fun getChildren(node: AccessibilityNodeInfo) = sequence {
|
||||||
if (root == null) {
|
repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { i ->
|
||||||
return emptyList()
|
val child = node.getChild(i) ?: return@sequence
|
||||||
}
|
yield(child)
|
||||||
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 const val MAX_KEEP_SIZE = 5000
|
||||||
|
|
||||||
if (!n.textQfInit && !n.attr.text.isNullOrEmpty()) {
|
// 先获取所有节点构建树结构, 然后再判断 idQf/textQf 如果存在一个能同时 idQf 和 textQf 的节点, 则认为 idQf 和 textQf 等价
|
||||||
n.textQf = (textQfCache[n.attr.text]
|
fun info2nodeList(root: AccessibilityNodeInfo?): List<NodeInfo> {
|
||||||
?: root.findAccessibilityNodeInfosByText(n.attr.text)).apply {
|
if (root == null) {
|
||||||
textQfCache[n.attr.text] = this
|
return emptyList()
|
||||||
}
|
}
|
||||||
.any { t -> t == n.node }
|
val nodes = mutableListOf<TempNodeData>()
|
||||||
}
|
val collectTime = measureTimeMillis {
|
||||||
|
val stack = mutableListOf<TempNodeData>()
|
||||||
if (n.idQf == true && n.textQf == true) {
|
var times = 0
|
||||||
idTextQf = true
|
stack.add(TempNodeData(root, null, 0, 0))
|
||||||
}
|
while (stack.isNotEmpty()) {
|
||||||
|
times++
|
||||||
if (!n.idQfInit && n.idQf != null) {
|
val node = stack.removeAt(stack.lastIndex)
|
||||||
n.parent?.children?.forEach { c ->
|
node.id = times - 1
|
||||||
c.idQf = n.idQf
|
val children = getChildren(node.node).mapIndexed { i, child ->
|
||||||
if (idTextQf) {
|
TempNodeData(
|
||||||
c.textQf = n.textQf
|
child, node, i, node.depth + 1
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
}.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 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"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
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.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
|
val cache = A11yContext(true)
|
||||||
) ?: throw RpcError("没有查询到节点")
|
|
||||||
|
val targetNode = serviceVal.safeActiveWindow?.let {
|
||||||
|
cache.rootCache = it
|
||||||
|
cache.querySelector(
|
||||||
|
it,
|
||||||
|
selector,
|
||||||
|
matchOption
|
||||||
|
)
|
||||||
|
} ?: throw RpcError("没有查询到节点")
|
||||||
|
|
||||||
if (gkdAction.action == null) {
|
if (gkdAction.action == null) {
|
||||||
// 仅查询
|
// 仅查询
|
||||||
|
@ -198,17 +204,18 @@ 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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()) }
|
||||||
|
|
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.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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -26,3 +26,5 @@ fun Bitmap.isEmptyBitmap(): Boolean {
|
||||||
}
|
}
|
||||||
return true
|
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.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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user