diff --git a/app/src/main/java/li/songe/gkd/data/ComplexSnapshot.kt b/app/src/main/java/li/songe/gkd/data/ComplexSnapshot.kt index 1d62f05..58723fd 100644 --- a/app/src/main/java/li/songe/gkd/data/ComplexSnapshot.kt +++ b/app/src/main/java/li/songe/gkd/data/ComplexSnapshot.kt @@ -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) diff --git a/app/src/main/java/li/songe/gkd/data/Rule.kt b/app/src/main/java/li/songe/gkd/data/Rule.kt index 5d2c2ef..fe80821 100644 --- a/app/src/main/java/li/songe/gkd/data/Rule.kt +++ b/app/src/main/java/li/songe/gkd/data/Rule.kt @@ -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 = emptySet(), val excludeActivityIds: Set = emptySet(), + val key: Int? = null, val preKeys: Set = 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 + } + ) + } \ No newline at end of file diff --git a/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt b/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt index cba5c24..750f7c7 100644 --- a/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt +++ b/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt @@ -126,6 +126,7 @@ data class SubscriptionRaw( val name: String? = null, val key: Int? = null, val preKeys: List = emptyList(), + val action: String? = null, override val cd: Long? = null, override val delay: Long? = null, override val activityIds: List? = null, @@ -241,6 +242,7 @@ data class SubscriptionRaw( appFilter = rulesJson["appFilter"]?.let { Singleton.json.decodeFromJsonElement(it) }, + action = getString(rulesJson, "action") ) } diff --git a/app/src/main/java/li/songe/gkd/debug/HttpService.kt b/app/src/main/java/li/songe/gkd/debug/HttpService.kt index c80ce92..b980f64 100644 --- a/app/src/main/java/li/songe/gkd/debug/HttpService.kt +++ b/app/src/main/java/li/songe/gkd/debug/HttpService.kt @@ -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 - call.respond(RpcOk(GkdAbService.click(text))) + val clickAction = call.receive() + LogUtils.d(clickAction) + call.respond(GkdAbService.execClickAction(clickAction)) } } } @@ -173,9 +176,6 @@ data class RpcOk( val message: String? = null, ) -@Serializable -data class Value(val value: T) - fun clearHttpSubs() { // 如果 app 被直接在任务列表划掉, HTTP订阅会没有清除, 所以在后续的第一次启动时清除 if (HttpService.isRunning()) return diff --git a/app/src/main/java/li/songe/gkd/service/AbExt.kt b/app/src/main/java/li/songe/gkd/service/AbExt.kt index 2457f49..0cf3d60 100644 --- a/app/src/main/java/li/songe/gkd/service/AbExt.kt +++ b/app/src/main/java/li/songe/gkd/service/AbExt.kt @@ -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 diff --git a/app/src/main/java/li/songe/gkd/service/GkdAbService.kt b/app/src/main/java/li/songe/gkd/service/GkdAbService.kt index 3201ecb..f27c854 100644 --- a/app/src/main/java/li/songe/gkd/service/GkdAbService.kt +++ b/app/src/main/java/li/songe/gkd/service/GkdAbService.kt @@ -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) { diff --git a/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt b/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt index 7ca724d..5d13fb5 100644 --- a/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt @@ -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 -> diff --git a/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt b/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt index 3166e70..cce4cfb 100644 --- a/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt +++ b/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt @@ -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( diff --git a/app/src/main/java/li/songe/gkd/ui/SubsPage.kt b/app/src/main/java/li/songe/gkd/ui/SubsPage.kt index 8999e38..8929f31 100644 --- a/app/src/main/java/li/songe/gkd/ui/SubsPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/SubsPage.kt @@ -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 = "添加")