feat: swipe action

This commit is contained in:
二刺螈
2026-04-01 22:01:13 +08:00
parent a5ee78e345
commit 2e4b058210
8 changed files with 252 additions and 77 deletions

View File

@@ -442,8 +442,9 @@ class A11yRuleEngine(val service: A11yCommonImpl) {
a, selector, MatchOption(fastQuery = gkdAction.fastQuery)
) ?: throw RpcError("没有查询到节点")
return withContext(Dispatchers.IO) {
ActionPerformer.getAction(gkdAction.action ?: ActionPerformer.None.action)
.perform(targetNode, gkdAction.position)
ActionPerformer
.getAction(gkdAction.action ?: ActionPerformer.None.action)
.perform(targetNode, gkdAction)
}
}

View File

@@ -16,27 +16,28 @@ data class GkdAction(
val selector: String,
val fastQuery: Boolean = false,
val action: String? = null,
val position: RawSubscription.Position? = null,
)
override val position: RawSubscription.Position? = null,
override val swipeArg: RawSubscription.SwipeArg? = null,
) : RawSubscription.LocationProps
@Serializable
data class ActionResult(
val action: String,
val result: Boolean,
val shizuku: Boolean = false,
val shell: Boolean = false,
val position: Pair<Float, Float>? = null,
)
sealed class ActionPerformer(val action: String) {
abstract fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
locationProps: RawSubscription.LocationProps,
): ActionResult
data object ClickNode : ActionPerformer("clickNode") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
locationProps: RawSubscription.LocationProps,
): ActionResult {
return ActionResult(
action = action,
@@ -48,20 +49,24 @@ sealed class ActionPerformer(val action: String) {
data object ClickCenter : ActionPerformer("clickCenter") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
locationProps: RawSubscription.LocationProps,
): ActionResult {
val rect = node.casted.boundsInScreen
val p = position?.calc(rect)
val p = locationProps.position?.calc(rect)
val x = p?.first ?: ((rect.right + rect.left) / 2f)
val y = p?.second ?: ((rect.bottom + rect.top) / 2f)
if (!ScreenUtils.inScreen(x, y)) {
return ActionResult(
action = action,
result = false,
position = x to y,
)
}
return ActionResult(
action = action,
result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) {
if (shizukuContextFlow.value.tap(x, y)) {
return ActionResult(
action = action, result = true, shizuku = true, position = x to y
)
}
result = if (shizukuContextFlow.value.tap(x, y)) {
true
} else {
val gestureDescription = GestureDescription.Builder()
val path = Path()
path.moveTo(x, y)
@@ -73,8 +78,6 @@ sealed class ActionPerformer(val action: String) {
A11yService.instance?.dispatchGesture(
gestureDescription.build(), null, null
) != null
} else {
false
},
position = x to y
)
@@ -84,65 +87,72 @@ sealed class ActionPerformer(val action: String) {
data object Click : ActionPerformer("click") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
locationProps: RawSubscription.LocationProps,
): ActionResult {
if (node.isClickable) {
val result = ClickNode.perform(node, position)
val result = ClickNode.perform(node, locationProps)
if (result.result) {
return result
}
}
return ClickCenter.perform(node, position)
return ClickCenter.perform(node, locationProps)
}
}
data object LongClickNode : ActionPerformer("longClickNode") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
locationProps: RawSubscription.LocationProps,
): ActionResult {
return ActionResult(
action = action,
result = node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
result = node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK).apply {
if (this) {
Thread.sleep(LongClickCenter.LONG_DURATION)
}
}
)
}
}
data object LongClickCenter : ActionPerformer("longClickCenter") {
const val LONG_DURATION = 500L
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
locationProps: RawSubscription.LocationProps,
): ActionResult {
val rect = node.casted.boundsInScreen
val p = position?.calc(rect)
val p = locationProps.position?.calc(rect)
val x = p?.first ?: ((rect.right + rect.left) / 2f)
val y = p?.second ?: ((rect.bottom + rect.top) / 2f)
// 某些系统的 ViewConfiguration.getLongPressTimeout() 返回 300 , 这将导致触发普通的 click 事件
val longClickDuration = 500L
if (!ScreenUtils.inScreen(x, y)) {
return ActionResult(
action = action,
result = false,
position = x to y,
)
}
return ActionResult(
action = action,
result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) {
if (shizukuContextFlow.value.tap(
x, y, longClickDuration
)
) {
return ActionResult(
action = action, result = true, shizuku = true, position = x to y
)
}
result = if (shizukuContextFlow.value.tap(x, y, LONG_DURATION)) {
true
} else {
val gestureDescription = GestureDescription.Builder()
val path = Path()
path.moveTo(x, y)
gestureDescription.addStroke(
GestureDescription.StrokeDescription(
path, 0, longClickDuration
path, 0, LONG_DURATION
)
)
A11yService.instance?.dispatchGesture(
(A11yService.instance?.dispatchGesture(
gestureDescription.build(), null, null
) != null
} else {
false
) != null).apply {
if (this) {
Thread.sleep(LONG_DURATION)
}
}
},
position = x to y
)
@@ -152,22 +162,22 @@ sealed class ActionPerformer(val action: String) {
data object LongClick : ActionPerformer("longClick") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
locationProps: RawSubscription.LocationProps,
): ActionResult {
if (node.isLongClickable) {
val result = LongClickNode.perform(node, position)
val result = LongClickNode.perform(node, locationProps)
if (result.result) {
return result
}
}
return LongClickCenter.perform(node, position)
return LongClickCenter.perform(node, locationProps)
}
}
data object Back : ActionPerformer("back") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
locationProps: RawSubscription.LocationProps,
): ActionResult {
return ActionResult(
action = action,
@@ -179,18 +189,89 @@ sealed class ActionPerformer(val action: String) {
data object None : ActionPerformer("none") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
locationProps: RawSubscription.LocationProps,
): ActionResult {
return ActionResult(
action = action, result = true
action = action,
result = true
)
}
}
data object Swipe : ActionPerformer("swipe") {
override fun perform(
node: AccessibilityNodeInfo,
locationProps: RawSubscription.LocationProps,
): ActionResult {
val rect = node.casted.boundsInScreen
val swipeArg = locationProps.swipeArg ?: return None.perform(node, locationProps)
val startP = swipeArg.start.calc(rect)
val endP = swipeArg.end?.calc(rect) ?: startP
if (startP == null || endP == null) {
return None.perform(node, locationProps)
}
val startX = startP.first
val startY = startP.second
val endX = endP.first
val endY = endP.second
if (!(ScreenUtils.inScreen(startX, startY) && ScreenUtils.inScreen(endX, endY))) {
return ActionResult(
action = action,
result = false,
position = endX to endY,
)
}
return if (shizukuContextFlow.value.swipe(
startX,
startY,
endX,
endY,
swipeArg.duration
)
) {
ActionResult(
action = action,
result = true,
shell = true,
position = endX to endY,
)
} else {
val gestureDescription = GestureDescription.Builder()
val path = Path()
path.moveTo(startX, startY)
path.lineTo(endX, endY)
gestureDescription.addStroke(
GestureDescription.StrokeDescription(
path, 0, swipeArg.duration
)
)
ActionResult(
action = action,
result = (A11yService.instance?.dispatchGesture(
gestureDescription.build(), null, null
) != null).apply {
if (this) {
Thread.sleep(swipeArg.duration)
}
},
position = endX to endY,
)
}
}
}
companion object {
private val allSubObjects by lazy {
arrayOf(
ClickNode, ClickCenter, Click, LongClickNode, LongClickCenter, LongClick, Back, None
ClickNode,
ClickCenter,
Click,
LongClickNode,
LongClickCenter,
LongClick,
Back,
None,
Swipe,
)
}

View File

@@ -14,9 +14,11 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import li.songe.gkd.a11y.A11yRuleEngine
import li.songe.gkd.a11y.typeInfo
import li.songe.gkd.util.LOCAL_SUBS_IDS
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.ScreenUtils
import li.songe.gkd.util.appInfoMapFlow
import li.songe.gkd.util.distinctByIfAny
import li.songe.gkd.util.filterIfNotAll
@@ -60,16 +62,6 @@ data class RawSubscription(
val hasRule get() = globalGroups.isNotEmpty() || apps.any { it.groups.isNotEmpty() }
val usedApps by lazy {
apps.run {
if (any { it.groups.isEmpty() }) {
filterNot { it.groups.isEmpty() }
} else {
this
}
}
}
fun getSafeCategory(key: Int): RawCategory {
return categories.find { it.key == key } ?: RawCategory(
key = key,
@@ -112,8 +104,6 @@ data class RawSubscription(
return categoryAppsMap[categoryKey] ?: emptyList()
}
fun getCategoryGroups(categoryKey: Int) = categoryGroupsMap[categoryKey] ?: emptyList()
fun getCategory(groupName: String): RawCategory? {
return categories.find { c -> groupName.startsWith(c.name) }
}
@@ -260,15 +250,37 @@ data class RawSubscription(
@Serializable
data class Position(
val left: String?, val top: String?, val right: String?, val bottom: String?
val left: String?,
val top: String?,
val right: String?,
val bottom: String?,
val x: String?,
val y: String?,
) {
private val leftExp by lazy { getExpression(left) }
private val topExp by lazy { getExpression(top) }
private val rightExp by lazy { getExpression(right) }
private val bottomExp by lazy { getExpression(bottom) }
private val xExp by lazy { getExpression(x) }
private val yExp by lazy { getExpression(y) }
val isValid by lazy {
((leftExp != null && (topExp != null || bottomExp != null)) || (rightExp != null && (topExp != null || bottomExp != null)))
arrayOf(
leftExp to topExp,
leftExp to bottomExp,
rightExp to topExp,
rightExp to bottomExp,
xExp to yExp,
).any {
it.first != null && it.second != null
}
}
private val isUseAppRect by lazy {
listOfNotNull(leftExp, topExp, rightExp, bottomExp, xExp, yExp).any { exp ->
exp.variableNames.any { v -> v == "appWidth" || v == "appHeight" }
}
}
/**
@@ -276,11 +288,23 @@ data class RawSubscription(
*/
fun calc(rect: Rect): Pair<Float, Float>? {
if (!isValid) return null
val appRect = if (isUseAppRect) {
Rect().apply {
A11yRuleEngine.service?.windowNodeInfo?.getBoundsInScreen(this)
}
} else {
null
}
arrayOf(
leftExp, topExp, rightExp, bottomExp
leftExp,
topExp,
rightExp,
bottomExp,
xExp,
yExp,
).forEach { exp ->
if (exp != null) {
setVariables(exp, rect)
setVariables(exp, rect, appRect)
}
}
try {
@@ -302,6 +326,10 @@ data class RawSubscription(
return (rect.right - rightExp!!.evaluate()
.toFloat()) to (rect.bottom - bottomExp!!.evaluate().toFloat())
}
} else if (xExp != null) {
if (yExp != null) {
return xExp!!.evaluate().toFloat() to yExp!!.evaluate().toFloat()
}
}
} catch (e: Exception) {
// 可能存在 1/0 导致错误
@@ -334,12 +362,26 @@ data class RawSubscription(
val priorityActionMaximum: Int?
}
sealed interface RawRuleProps : RawCommonProps {
@Serializable
data class SwipeArg(
val start: Position,
val end: Position?,
val duration: Long,
)
interface LocationProps {
// click
val position: Position?
// swipe
val swipeArg: SwipeArg?
}
sealed interface RawRuleProps : RawCommonProps, LocationProps {
val name: String?
val key: Int?
val preKeys: List<Int>?
val action: String?
val position: Position?
val matches: List<String>?
val anyMatches: List<String>?
val excludeMatches: List<String>?
@@ -499,6 +541,7 @@ data class RawSubscription(
override val preKeys: List<Int>?,
override val action: String?,
override val position: Position?,
override val swipeArg: SwipeArg?,
override val matches: List<String>?,
override val excludeMatches: List<String>?,
override val excludeAllMatches: List<String>?,
@@ -559,6 +602,7 @@ data class RawSubscription(
override val preKeys: List<Int>?,
override val action: String?,
override val position: Position?,
override val swipeArg: SwipeArg?,
override val matches: List<String>?,
override val excludeMatches: List<String>?,
override val excludeAllMatches: List<String>?,
@@ -623,7 +667,7 @@ data class RawSubscription(
"random"
)
private fun setVariables(exp: Expression, rect: Rect) {
private fun setVariables(exp: Expression, rect: Rect, appRect: Rect?) {
exp.setVariable("left", rect.left.toDouble())
exp.setVariable("top", rect.top.toDouble())
exp.setVariable("right", rect.right.toDouble())
@@ -631,6 +675,12 @@ data class RawSubscription(
exp.setVariable("width", rect.width().toDouble())
exp.setVariable("height", rect.height().toDouble())
exp.setVariable("random", Math.random())
exp.setVariable("screenWidth", ScreenUtils.getScreenWidth().toDouble())
exp.setVariable("screenHeight", ScreenUtils.getScreenHeight().toDouble())
if (appRect != null) {
exp.setVariable("appWidth", appRect.width().toDouble())
exp.setVariable("appHeight", appRect.height().toDouble())
}
}
private fun getExpression(value: String?): Expression? {
@@ -657,22 +707,37 @@ data class RawSubscription(
}
}
private fun getPosition(jsonObject: JsonObject?): Position? {
return when (val element = jsonObject?.get("position")) {
JsonNull, null -> null
is JsonObject -> {
Position(
left = element["left"]?.jsonPrimitive?.content,
bottom = element["bottom"]?.jsonPrimitive?.content,
top = element["top"]?.jsonPrimitive?.content,
right = element["right"]?.jsonPrimitive?.content,
)
}
private fun getPosition(jsonObject: JsonObject?, useSelf: Boolean = false): Position? {
return when (val element = if (useSelf) jsonObject else jsonObject?.get("position")) {
is JsonObject -> Position(
left = element["left"]?.jsonPrimitive?.content,
bottom = element["bottom"]?.jsonPrimitive?.content,
top = element["top"]?.jsonPrimitive?.content,
right = element["right"]?.jsonPrimitive?.content,
x = element["x"]?.jsonPrimitive?.content,
y = element["y"]?.jsonPrimitive?.content,
)
else -> null
}
}
private fun getSwipeArg(
jsonObject: JsonObject?
): SwipeArg? = when (val element = jsonObject?.get("swipeArg")) {
is JsonObject -> {
SwipeArg(
start = getPosition(element["start"]?.jsonObject, true)
?: error("swipe start position is required"),
end = getPosition(element["end"]?.jsonObject, true),
duration = element["duration"]?.jsonPrimitive?.long
?: error("swipe duration is required"),
)
}
else -> null
}
private fun getStringIArray(jsonObject: JsonObject?, name: String): List<String>? {
return when (val element = jsonObject?.get(name)) {
JsonNull, null -> null
@@ -841,6 +906,7 @@ data class RawSubscription(
versionCode = getCompatVersionCode(jsonObject),
versionName = getCompatVersionName(jsonObject),
position = getPosition(jsonObject),
swipeArg = getSwipeArg(jsonObject),
forcedTime = getLong(jsonObject, "forcedTime"),
priorityTime = getLong(jsonObject, "priorityTime"),
priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"),
@@ -958,6 +1024,7 @@ data class RawSubscription(
order = getInt(jsonObject, "order"),
forcedTime = getLong(jsonObject, "forcedTime"),
position = getPosition(jsonObject),
swipeArg = getSwipeArg(jsonObject),
priorityTime = getLong(jsonObject, "priorityTime"),
priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"),
)

View File

@@ -159,10 +159,12 @@ sealed class ResolvedRule(
private val performer = ActionPerformer.getAction(rule.action ?: rule.position?.let {
ActionPerformer.ClickCenter.action
} ?: rule.swipeArg?.let {
ActionPerformer.Swipe.action
})
fun performAction(node: AccessibilityNodeInfo): ActionResult {
return performer.perform(node, rule.position)
return performer.perform(node, rule)
}
val matchDelayJob = atomic<Job?>(null)

View File

@@ -37,6 +37,11 @@ class SafeInputManager(private val value: IInputManager) {
}
}
@WorkerThread
fun swipe(x1: Float, y1: Float, x2: Float, y2: Float, duration: Long) {
command.runSwipe(x1, y1, x2, y2, duration)
}
fun key(keyCode: Int) = command.runKeyEvent(keyCode)
}

View File

@@ -144,6 +144,16 @@ class ShizukuContext(
return serviceWrapper?.tap(x, y, duration) ?: (inputManager?.tap(x, y, duration) != null)
}
fun swipe(x1: Float, y1: Float, x2: Float, y2: Float, duration: Long): Boolean {
return serviceWrapper?.swipe(x1, y1, x2, y2, duration) ?: (inputManager?.swipe(
x1,
y1,
x2,
y2,
duration
) != null)
}
fun topCpn(): ComponentName? {
return (activityTaskManager?.getTasks()
?: activityManager?.getTasks())?.firstOrNull()?.topActivity

View File

@@ -165,6 +165,11 @@ data class UserServiceWrapper(
return execCommandForResult(command).ok
}
fun swipe(x1: Float, y1: Float, x2: Float, y2: Float, duration: Long): Boolean {
val command = "input swipe $x1 $y1 $x2 $y2 $duration"
return execCommandForResult(command).ok
}
fun screencapFile(filePath: String): Boolean {
val tempPath = "/data/local/tmp/screencap_${System.currentTimeMillis()}.png"
val command = "screencap -p $tempPath"

View File

@@ -22,4 +22,8 @@ object ScreenUtils {
}
fun isScreenLock(): Boolean = app.keyguardManager.inKeyguardRestrictedInputMode()
fun inScreen(x: Float, y: Float): Boolean {
return 0 <= x && 0 <= y && x <= getScreenWidth() && y <= getScreenHeight()
}
}