feat: 支持自定义点击 action

This commit is contained in:
lisonge 2023-09-18 23:35:00 +08:00
parent 592415edff
commit bf8c2455fc
9 changed files with 113 additions and 59 deletions

View File

@ -4,6 +4,7 @@ import com.blankj.utilcode.util.AppUtils
import com.blankj.utilcode.util.ScreenUtils
import kotlinx.serialization.Serializable
import li.songe.gkd.service.GkdAbService
import li.songe.gkd.service.safeActiveWindow
import li.songe.gkd.service.topActivityFlow
@Serializable
@ -26,7 +27,7 @@ data class ComplexSnapshot(
fun createComplexSnapshot(): ComplexSnapshot {
val currentAbNode = GkdAbService.currentAbNode
val currentAbNode = GkdAbService.service?.safeActiveWindow
val appId = currentAbNode?.packageName?.toString()
val currentActivityId = topActivityFlow.value?.activityId
val appInfo = if (appId == null) null else AppUtils.getAppInfo(appId)

View File

@ -1,6 +1,11 @@
package li.songe.gkd.data
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.graphics.Path
import android.graphics.Rect
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.serialization.Serializable
import li.songe.gkd.service.lastTriggerRuleFlow
import li.songe.gkd.service.querySelector
import li.songe.selector.Selector
@ -18,11 +23,14 @@ data class Rule(
val cd: Long = defaultMiniCd,
val delay: Long = 0,
val index: Int = 0,
val appId: String = "",
val activityIds: Set<String> = emptySet(),
val excludeActivityIds: Set<String> = emptySet(),
val key: Int? = null,
val preKeys: Set<Int> = emptySet(),
val rule: SubscriptionRaw.RuleRaw,
val group: SubscriptionRaw.GroupRaw,
val app: SubscriptionRaw.AppRaw,
@ -68,7 +76,59 @@ data class Rule(
return activityIds.any { activityId.startsWith(it) }
}
val performAction: ActionFc = when (rule.action) {
"clickNode" -> clickNode
"clickCenter" -> clickCenter
else -> click
}
companion object {
const val defaultMiniCd = 1000L
}
}
typealias ActionFc = (context: AccessibilityService, node: AccessibilityNodeInfo) -> ActionResult
@Serializable
data class ClickAction(
val selector: String,
val action: String? = null,
)
@Serializable
data class ActionResult(
val action: String,
val result: Boolean,
)
val click: ActionFc = { context, node ->
if (node.isClickable) clickNode(context, node) else clickCenter(context, node)
}
val clickNode: ActionFc = { _, node ->
ActionResult(
action = "clickNode", result = node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
)
}
val clickCenter: ActionFc = { context, node ->
val react = Rect()
node.getBoundsInScreen(react)
val x = react.left + 50f / 100f * (react.right - react.left)
val y = react.top + 50f / 100f * (react.bottom - react.top)
ActionResult(
action = "clickCenter", result = if (x >= 0 && y >= 0) {
val gestureDescription = GestureDescription.Builder()
val path = Path()
path.moveTo(x, y)
gestureDescription.addStroke(GestureDescription.StrokeDescription(path, 0, 300))
context.dispatchGesture(gestureDescription.build(), null, null)
true
} else {
false
}
)
}

View File

@ -126,6 +126,7 @@ data class SubscriptionRaw(
val name: String? = null,
val key: Int? = null,
val preKeys: List<Int> = emptyList(),
val action: String? = null,
override val cd: Long? = null,
override val delay: Long? = null,
override val activityIds: List<String>? = null,
@ -241,6 +242,7 @@ data class SubscriptionRaw(
appFilter = rulesJson["appFilter"]?.let {
Singleton.json.decodeFromJsonElement(it)
},
action = getString(rulesJson, "action")
)
}

View File

@ -32,6 +32,7 @@ import kotlinx.serialization.Serializable
import li.songe.gkd.app
import li.songe.gkd.appScope
import li.songe.gkd.composition.CompositionService
import li.songe.gkd.data.ClickAction
import li.songe.gkd.data.DeviceInfo
import li.songe.gkd.data.RpcError
import li.songe.gkd.data.SubsItem
@ -43,6 +44,7 @@ import li.songe.gkd.notif.httpChannel
import li.songe.gkd.notif.httpNotif
import li.songe.gkd.service.GkdAbService
import li.songe.gkd.util.Ext.getIpAddressInLocalNetwork
import li.songe.gkd.util.Singleton
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.storeFlow
import li.songe.gkd.util.subsItemsFlow
@ -65,7 +67,7 @@ class HttpService : CompositionService({
return embeddedServer(Netty, port, configure = { tcpKeepAlive = true }) {
install(KtorCorsPlugin)
install(KtorErrorPlugin)
install(ContentNegotiation) { json() }
install(ContentNegotiation) { json(Singleton.json) }
routing {
get("/") { call.respond("hello world") }
@ -118,8 +120,9 @@ class HttpService : CompositionService({
if (!GkdAbService.isRunning()) {
throw RpcError("无障碍没有运行")
}
val text = call.receive<Value<String>>().value
call.respond(RpcOk(GkdAbService.click(text)))
val clickAction = call.receive<ClickAction>()
LogUtils.d(clickAction)
call.respond(GkdAbService.execClickAction(clickAction))
}
}
}
@ -173,9 +176,6 @@ data class RpcOk(
val message: String? = null,
)
@Serializable
data class Value<T>(val value: T)
fun clearHttpSubs() {
// 如果 app 被直接在任务列表划掉, HTTP订阅会没有清除, 所以在后续的第一次启动时清除
if (HttpService.isRunning()) return

View File

@ -1,8 +1,6 @@
package li.songe.gkd.service
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.graphics.Path
import android.graphics.Rect
import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.Selector
@ -37,30 +35,11 @@ inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode:
}
}
fun AccessibilityNodeInfo.click(service: AccessibilityService) = when {
this.isClickable -> {
this.performAction(AccessibilityNodeInfo.ACTION_CLICK)
"self"
}
else -> {
val react = Rect()
this.getBoundsInScreen(react)
val x = react.left + 50f / 100f * (react.right - react.left)
val y = react.top + 50f / 100f * (react.bottom - react.top)
if (x >= 0 && y >= 0) {
val gestureDescription = GestureDescription.Builder()
val path = Path()
path.moveTo(x, y)
gestureDescription.addStroke(GestureDescription.StrokeDescription(path, 0, 300))
service.dispatchGesture(gestureDescription.build(), null, null)
"(50%, 50%)"
} else {
null
}
}
}
/**
* 此方法小概率造成无限节点片段,底层原因未知
*
* https://github.com/gkd-kit/gkd/issues/28
*/
fun AccessibilityNodeInfo.getDepth(): Int {
var p: AccessibilityNodeInfo? = this
var depth = 0

View File

@ -6,7 +6,6 @@ import android.graphics.Bitmap
import android.os.Build
import android.view.Display
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.NetworkUtils
import com.blankj.utilcode.util.ScreenUtils
@ -28,10 +27,15 @@ import kotlinx.coroutines.launch
import li.songe.gkd.composition.CompositionAbService
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
import li.songe.gkd.composition.CompositionExt.useScope
import li.songe.gkd.data.ActionResult
import li.songe.gkd.data.ClickAction
import li.songe.gkd.data.ClickLog
import li.songe.gkd.data.NodeInfo
import li.songe.gkd.data.RpcError
import li.songe.gkd.data.SubscriptionRaw
import li.songe.gkd.data.click
import li.songe.gkd.data.clickCenter
import li.songe.gkd.data.clickNode
import li.songe.gkd.db.DbSet
import li.songe.gkd.shizuku.newActivityTaskManager
import li.songe.gkd.shizuku.shizukuIsSafeOK
@ -178,8 +182,9 @@ class GkdAbService : CompositionAbService({
}
// 如果节点在屏幕外部, click 的结果为 null
val clickResult = target.click(context)
if (clickResult != null) {
val actionResult = rule.performAction(context, target)
if (actionResult.result) {
if (storeFlow.value.toastWhenClick) {
val t = System.currentTimeMillis()
if (t - lastToastTime > 3000) {
@ -189,7 +194,7 @@ class GkdAbService : CompositionAbService({
}
rule.trigger()
LogUtils.d(
*rule.matches.toTypedArray(), NodeInfo.abNodeToNode(target).attr, clickResult
*rule.matches.toTypedArray(), NodeInfo.abNodeToNode(target).attr, actionResult
)
scope.launchTry(IO) {
val clickLog = ClickLog(
@ -278,25 +283,29 @@ class GkdAbService : CompositionAbService({
}) {
companion object {
private var service: GkdAbService? = null
var service: GkdAbService? = null
fun isRunning() = ServiceUtils.isServiceRunning(GkdAbService::class.java)
fun click(source: String): String? {
fun execClickAction(clickAction: ClickAction): ActionResult {
val serviceVal = service ?: throw RpcError("无障碍没有运行")
val selector = try {
Selector.parse(source)
Selector.parse(clickAction.selector)
} catch (e: Exception) {
throw RpcError("非法选择器")
}
return currentAbNode?.querySelector(selector)?.click(serviceVal)
val targetNode = serviceVal.safeActiveWindow?.querySelector(selector) ?: throw RpcError(
"没有选择到节点"
)
return when (clickAction.action) {
"clickNode" -> clickNode(serviceVal, targetNode)
"clickCenter" -> clickCenter(serviceVal, targetNode)
else -> click(serviceVal, targetNode)
}
}
val currentAbNode: AccessibilityNodeInfo?
get() {
return service?.safeActiveWindow
}
suspend fun currentScreenshot() = service?.run {
suspendCoroutine {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

View File

@ -45,6 +45,7 @@ import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.ClipboardUtils
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.ToastUtils
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
@ -87,7 +88,7 @@ fun AppItemPage(
)
}
val editable = subsItemId == -2L
val editable = subsItemId < 0
var showAddDlg by remember { mutableStateOf(false) }
@ -311,8 +312,8 @@ fun AppItemPage(
val newGroupRaw = try {
SubscriptionRaw.parseGroupRaw(source)
} catch (e: Exception) {
e.printStackTrace()
ToastUtils.showShort("非法规则")
LogUtils.d(e)
ToastUtils.showShort("非法规则:${e.message}")
return@TextButton
}
if (newGroupRaw.key != editGroupRaw.key) {
@ -320,7 +321,7 @@ fun AppItemPage(
return@TextButton
}
if (!newGroupRaw.valid) {
ToastUtils.showShort("非法规则")
ToastUtils.showShort("非法规则:存在非法选择器")
return@TextButton
}
setEditGroupRaw(null)
@ -384,8 +385,9 @@ fun AppItemPage(
val tempGroups = if (newAppRaw == null) {
val newGroupRaw = try {
SubscriptionRaw.parseGroupRaw(source)
} catch (_: Exception) {
ToastUtils.showShort("非法规则")
} catch (e: Exception) {
LogUtils.d(e)
ToastUtils.showShort("非法规则:${e.message}")
return@TextButton
}
listOf(newGroupRaw)
@ -401,7 +403,7 @@ fun AppItemPage(
newAppRaw.groups
}
if (!tempGroups.all { g -> g.valid }) {
ToastUtils.showShort("存在非法规则")
ToastUtils.showShort("非法规则:存在非法选择器")
return@TextButton
}
tempGroups.forEach { g ->

View File

@ -57,7 +57,7 @@ class SubsManageVm @Inject constructor() : ViewModel() {
return@launch
}
if (newSubsRaw.id < 0) {
ToastUtils.showShort("订阅id不可小于0")
ToastUtils.showShort("订阅id不可${newSubsRaw.id}\n负数id为内部使用")
return@launch
}
val newItem = SubsItem(

View File

@ -43,6 +43,7 @@ import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.ClipboardUtils
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.ToastUtils
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
@ -84,7 +85,7 @@ fun SubsPage(
val subsRaw = subsIdToRaw[subsItem?.id]
// 本地订阅
val editable = subsItem?.id == -2L
val editable = subsItem?.id.let { it != null && it < 0 }
var showDetailDlg by remember {
mutableStateOf(false)
@ -240,8 +241,8 @@ fun SubsPage(
val newAppRaw = try {
SubscriptionRaw.parseAppRaw(source)
} catch (e: Exception) {
e.printStackTrace()
ToastUtils.showShort("非法规则")
LogUtils.d(e)
ToastUtils.showShort("非法规则${e.message}")
return@TextButton
}
if (newAppRaw.groups.any { s -> s.name.isBlank() }) {
@ -344,8 +345,8 @@ fun SubsPage(
ToastUtils.showShort("更新成功")
}
} catch (e: Exception) {
e.printStackTrace()
ToastUtils.showShort("非法规则")
LogUtils.d(e)
ToastUtils.showShort("非法规则${e.message}")
}
}, enabled = source.isNotEmpty()) {
Text(text = "添加")