mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
feat: 支持自定义点击 action
This commit is contained in:
parent
592415edff
commit
bf8c2455fc
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
||||
}
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 = "添加")
|
||||
|
|
Loading…
Reference in New Issue
Block a user