feat: position

This commit is contained in:
lisonge 2024-02-18 02:03:15 +08:00
parent c711ccfd06
commit a726e2b22b
6 changed files with 176 additions and 38 deletions

View File

@ -216,4 +216,6 @@ dependencies {
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.coil.gif) implementation(libs.coil.gif)
implementation(libs.exp4j)
} }

View File

@ -10,7 +10,7 @@ import com.blankj.utilcode.util.ScreenUtils
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
typealias ActionFc = (context: AccessibilityService, node: AccessibilityNodeInfo) -> ActionResult typealias ActionFc = (context: AccessibilityService, node: AccessibilityNodeInfo, position: RawSubscription.Position?) -> ActionResult
@Serializable @Serializable
@ -18,6 +18,7 @@ data class GkdAction(
val selector: String, val selector: String,
val quickFind: Boolean = false, val quickFind: Boolean = false,
val action: String? = null, val action: String? = null,
val position: RawSubscription.Position? = null
) )
@Serializable @Serializable
@ -26,19 +27,21 @@ data class ActionResult(
val result: Boolean, val result: Boolean,
) )
val clickNode: ActionFc = { _, node -> val clickNode: ActionFc = { _, node, _ ->
ActionResult( ActionResult(
action = "clickNode", result = node.performAction(AccessibilityNodeInfo.ACTION_CLICK) action = "clickNode", result = node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
) )
} }
val clickCenter: ActionFc = { context, node -> val clickCenter: ActionFc = { context, node, position ->
val react = Rect() val rect = Rect()
node.getBoundsInScreen(react) node.getBoundsInScreen(rect)
val x = (react.right + react.left) / 2f val p = position?.calc(rect)
val y = (react.bottom + react.top) / 2f val x = p?.first ?: ((rect.right + rect.left) / 2f)
val y = p?.second ?: ((rect.bottom + rect.top) / 2f)
ActionResult( ActionResult(
action = "clickCenter", action = "clickCenter",
// TODO 在分屏/小窗模式下会点击到应用界面外部导致误触其它应用
result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) { result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) {
val gestureDescription = GestureDescription.Builder() val gestureDescription = GestureDescription.Builder()
val path = Path() val path = Path()
@ -56,31 +59,32 @@ val clickCenter: ActionFc = { context, node ->
) )
} }
val click: ActionFc = { context, node -> val click: ActionFc = { context, node, position ->
if (node.isClickable) { if (node.isClickable) {
val result = clickNode(context, node) val result = clickNode(context, node, position)
if (result.result) { if (result.result) {
result result
} else { } else {
clickCenter(context, node) clickCenter(context, node, position)
} }
} else { } else {
clickCenter(context, node) clickCenter(context, node, position)
} }
} }
val longClickNode: ActionFc = { _, node -> val longClickNode: ActionFc = { _, node, _ ->
ActionResult( ActionResult(
action = "longClickNode", action = "longClickNode",
result = node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK) result = node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
) )
} }
val longClickCenter: ActionFc = { context, node -> val longClickCenter: ActionFc = { context, node, position ->
val react = Rect() val rect = Rect()
node.getBoundsInScreen(react) node.getBoundsInScreen(rect)
val x = (react.right + react.left) / 2f val p = position?.calc(rect)
val y = (react.bottom + react.top) / 2f val x = p?.first ?: ((rect.right + rect.left) / 2f)
val y = p?.second ?: ((rect.bottom + rect.top) / 2f)
// 内部的 DEFAULT_LONG_PRESS_TIMEOUT 常量是 400 // 内部的 DEFAULT_LONG_PRESS_TIMEOUT 常量是 400
// 而 ViewConfiguration.getLongPressTimeout() 返回 300, 这将导致触发普通的 click 事件 // 而 ViewConfiguration.getLongPressTimeout() 返回 300, 这将导致触发普通的 click 事件
ActionResult( ActionResult(
@ -104,20 +108,20 @@ val longClickCenter: ActionFc = { context, node ->
} }
val longClick: ActionFc = { context, node -> val longClick: ActionFc = { context, node, position ->
if (node.isLongClickable) { if (node.isLongClickable) {
val result = longClickNode(context, node) val result = longClickNode(context, node, position)
if (result.result) { if (result.result) {
result result
} else { } else {
longClickCenter(context, node) longClickCenter(context, node, position)
} }
} else { } else {
longClickCenter(context, node) longClickCenter(context, node, position)
} }
} }
val backFc: ActionFc = { context, _ -> val backFc: ActionFc = { context, _, _ ->
ActionResult( ActionResult(
action = "back", action = "back",
result = context.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) result = context.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)

View File

@ -1,5 +1,6 @@
package li.songe.gkd.data package li.songe.gkd.data
import android.graphics.Rect
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
@ -11,11 +12,14 @@ import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long import kotlinx.serialization.json.long
import li.songe.gkd.service.allowPropertyNames import li.songe.gkd.service.allowPropertyNames
import li.songe.gkd.util.json import li.songe.gkd.util.json
import li.songe.gkd.util.json5ToJson import li.songe.gkd.util.json5ToJson
import li.songe.selector.Selector import li.songe.selector.Selector
import net.objecthunter.exp4j.Expression
import net.objecthunter.exp4j.ExpressionBuilder
@Immutable @Immutable
@Serializable @Serializable
@ -90,6 +94,61 @@ data class RawSubscription(
@Serializable @Serializable
data class RawCategory(val key: Int, val name: String, val enable: Boolean?) data class RawCategory(val key: Int, val name: String, val enable: Boolean?)
@Immutable
@Serializable
data class Position(
val left: String?,
val top: String?,
val right: String?,
val bottom: 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) }
val isValid by lazy {
((leftExp != null && (topExp != null || bottomExp != null)) || (rightExp != null && (topExp != null || bottomExp != null)))
}
/**
* return (x, y)
*/
fun calc(rect: Rect): Pair<Float, Float>? {
if (!isValid) return null
arrayOf(
leftExp,
topExp,
rightExp,
bottomExp
).forEach { exp ->
if (exp != null) {
setVariables(exp, rect)
}
}
if (leftExp != null) {
if (topExp != null) {
return (rect.left + leftExp!!.evaluate()
.toFloat()) to (rect.top + topExp!!.evaluate().toFloat())
}
if (bottomExp != null) {
return (rect.left + leftExp!!.evaluate()
.toFloat()) to (rect.bottom - bottomExp!!.evaluate().toFloat())
}
} else if (rightExp != null) {
if (topExp != null) {
return (rect.right - rightExp!!.evaluate()
.toFloat()) to (rect.top + topExp!!.evaluate().toFloat())
}
if (bottomExp != null) {
return (rect.right - rightExp!!.evaluate()
.toFloat()) to (rect.bottom - bottomExp!!.evaluate().toFloat())
}
}
return null
}
}
interface RawCommonProps { interface RawCommonProps {
val actionCd: Long? val actionCd: Long?
@ -111,6 +170,7 @@ data class RawSubscription(
val key: Int? val key: Int?
val preKeys: List<Int>? val preKeys: List<Int>?
val action: String? val action: String?
val position: Position?
val matches: List<String>? val matches: List<String>?
val excludeMatches: List<String>? val excludeMatches: List<String>?
} }
@ -208,6 +268,9 @@ data class RawSubscription(
unknownPropertyNames.forEach { n -> unknownPropertyNames.forEach { n ->
return@lazy "非法属性名:${n}" return@lazy "非法属性名:${n}"
} }
if (rules.any { r -> r.position?.isValid == false }) {
return@lazy "非法位置"
}
null null
} }
@ -239,6 +302,7 @@ data class RawSubscription(
override val key: Int?, override val key: Int?,
override val preKeys: List<Int>?, override val preKeys: List<Int>?,
override val action: String?, override val action: String?,
override val position: Position?,
override val matches: List<String>, override val matches: List<String>,
override val excludeMatches: List<String>?, override val excludeMatches: List<String>?,
override val matchAnyApp: Boolean?, override val matchAnyApp: Boolean?,
@ -318,6 +382,7 @@ data class RawSubscription(
override val key: Int?, override val key: Int?,
override val preKeys: List<Int>?, override val preKeys: List<Int>?,
override val action: String?, override val action: String?,
override val position: Position?,
override val matches: List<String>?, override val matches: List<String>?,
override val excludeMatches: List<String>?, override val excludeMatches: List<String>?,
@ -345,11 +410,69 @@ data class RawSubscription(
companion object { companion object {
private val expVars = arrayOf(
"left",
"top",
"right",
"bottom",
"width",
"height"
)
private fun setVariables(exp: Expression, rect: Rect) {
exp.setVariable("left", rect.left.toDouble())
exp.setVariable("top", rect.top.toDouble())
exp.setVariable("right", rect.right.toDouble())
exp.setVariable("bottom", rect.bottom.toDouble())
exp.setVariable("width", rect.width().toDouble())
exp.setVariable("height", rect.height().toDouble())
}
private fun getExpression(value: String?): Expression? {
return if (value != null) {
try {
ExpressionBuilder(value).variables(*expVars).build().apply {
expVars.forEach { v ->
// 预填充作 validate
setVariable(v, 0.0)
}
}.let { e ->
if (e.validate().isValid) {
e
} else {
null
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
} else {
null
}
}
private fun getPosition(jsonObject: JsonObject? = null): 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,
)
}
else -> null
}
}
private fun getStringIArray( private fun getStringIArray(
json: JsonObject? = null, jsonObject: JsonObject? = null,
name: String name: String
): List<String>? { ): List<String>? {
return when (val element = json?.get(name)) { return when (val element = jsonObject?.get(name)) {
JsonNull, null -> null JsonNull, null -> null
is JsonObject -> error("Element ${this::class} can not be object") is JsonObject -> error("Element ${this::class} can not be object")
is JsonArray -> element.map { is JsonArray -> element.map {
@ -363,8 +486,8 @@ data class RawSubscription(
} }
} }
private fun getIntIArray(json: JsonObject? = null, name: String): List<Int>? { private fun getIntIArray(jsonObject: JsonObject? = null, name: String): List<Int>? {
return when (val element = json?.get(name)) { return when (val element = jsonObject?.get(name)) {
JsonNull, null -> null JsonNull, null -> null
is JsonArray -> element.map { is JsonArray -> element.map {
when (it) { when (it) {
@ -378,8 +501,8 @@ data class RawSubscription(
} }
} }
private fun getLongIArray(json: JsonObject? = null, name: String): List<Long>? { private fun getLongIArray(jsonObject: JsonObject? = null, name: String): List<Long>? {
return when (val element = json?.get(name)) { return when (val element = jsonObject?.get(name)) {
JsonNull, null -> null JsonNull, null -> null
is JsonArray -> element.map { is JsonArray -> element.map {
when (it) { when (it) {
@ -393,8 +516,8 @@ data class RawSubscription(
} }
} }
private fun getString(json: JsonObject? = null, key: String): String? = private fun getString(jsonObject: JsonObject? = null, key: String): String? =
when (val p = json?.get(key)) { when (val p = jsonObject?.get(key)) {
JsonNull, null -> null JsonNull, null -> null
is JsonPrimitive -> { is JsonPrimitive -> {
if (p.isString) { if (p.isString) {
@ -407,8 +530,8 @@ data class RawSubscription(
else -> error("Element $p is not a string") else -> error("Element $p is not a string")
} }
private fun getLong(json: JsonObject? = null, key: String): Long? = private fun getLong(jsonObject: JsonObject? = null, key: String): Long? =
when (val p = json?.get(key)) { when (val p = jsonObject?.get(key)) {
JsonNull, null -> null JsonNull, null -> null
is JsonPrimitive -> { is JsonPrimitive -> {
p.long p.long
@ -417,8 +540,8 @@ data class RawSubscription(
else -> error("Element $p is not a long") else -> error("Element $p is not a long")
} }
private fun getInt(json: JsonObject? = null, key: String): Int? = private fun getInt(jsonObject: JsonObject? = null, key: String): Int? =
when (val p = json?.get(key)) { when (val p = jsonObject?.get(key)) {
JsonNull, null -> null JsonNull, null -> null
is JsonPrimitive -> { is JsonPrimitive -> {
p.int p.int
@ -427,8 +550,8 @@ data class RawSubscription(
else -> error("Element $p is not a int") else -> error("Element $p is not a int")
} }
private fun getBoolean(json: JsonObject? = null, key: String): Boolean? = private fun getBoolean(jsonObject: JsonObject? = null, key: String): Boolean? =
when (val p = json?.get(key)) { when (val p = jsonObject?.get(key)) {
JsonNull, null -> null JsonNull, null -> null
is JsonPrimitive -> { is JsonPrimitive -> {
p.boolean p.boolean
@ -468,6 +591,7 @@ 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"),
position = getPosition(jsonObject),
) )
} }
@ -568,6 +692,7 @@ data class RawSubscription(
excludeMatches = getStringIArray(jsonObject, "excludeMatches"), excludeMatches = getStringIArray(jsonObject, "excludeMatches"),
matches = getStringIArray(jsonObject, "matches") ?: error("miss matches"), matches = getStringIArray(jsonObject, "matches") ?: error("miss matches"),
order = getInt(jsonObject, "order"), order = getInt(jsonObject, "order"),
position = getPosition(jsonObject),
) )
} }

View File

@ -1,5 +1,6 @@
package li.songe.gkd.data package li.songe.gkd.data
import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import li.songe.gkd.service.lastTriggerRule import li.songe.gkd.service.lastTriggerRule
@ -130,7 +131,10 @@ sealed class ResolvedRule(
return target return target
} }
val performAction = getActionFc(rule.action) private val fc = getActionFc(rule.action)
fun performAction(context: AccessibilityService, node: AccessibilityNodeInfo): ActionResult {
return fc(context, node, rule.position)
}
var matchDelayJob: Job? = null var matchDelayJob: Job? = null

View File

@ -533,7 +533,7 @@ class GkdAbService : CompositionAbService({
) )
} }
return getActionFc(gkdAction.action)(serviceVal, targetNode) return getActionFc(gkdAction.action)(serviceVal, targetNode, gkdAction.position)
} }

View File

@ -204,6 +204,9 @@ dependencyResolutionManagement {
// https://github.com/Calvin-LL/Reorderable // https://github.com/Calvin-LL/Reorderable
library("others.reorderable", "sh.calvin.reorderable:reorderable:1.3.1") library("others.reorderable", "sh.calvin.reorderable:reorderable:1.3.1")
// https://www.objecthunter.net/exp4j/
library("exp4j", "net.objecthunter:exp4j:0.4.8")
} }
} }
} }