From 666d19ad664add07531310939abead343b08887d Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Mon, 7 Sep 2026 10:21:36 +0800 Subject: [PATCH] fix(agent): distinguish nested purchase scroll containers (#230) --- android/app/build.gradle.kts | 4 +- .../automation/GoAutoAccessibilityService.kt | 30 +++++++++----- .../agent/automation/PurchaseScrollLocator.kt | 24 ++++++++++++ .../goauto/agent/PurchaseScrollLocatorTest.kt | 39 +++++++++++++++++++ 4 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseScrollLocator.kt create mode 100644 android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseScrollLocatorTest.kt diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index cd2f19c..043d059 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "cn.ilapage.goauto.agent" minSdk = 23 targetSdk = 34 - versionCode = 68 - versionName = "0.9.55" + versionCode = 69 + versionName = "0.9.56" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt index a5a3f06..3f8bedb 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt @@ -450,17 +450,27 @@ class GoAutoAccessibilityService : AccessibilityService(), UiDriver, PddCollecto override fun swipePurchaseIn(target: SnapshotNode, direction: SwipeDirection, durationMs: Long): Boolean { val root = rootInActiveWindow ?: return false - val matches = mutableListOf() - walk(root) { node -> - val bounds = Rect().also(node::getBoundsInScreen) - if (node.isVisibleToUser && node.isEnabled && node.isScrollable && - node.className?.toString() == target.className && - kotlin.math.abs(bounds.centerX() - target.bounds.centerX) <= 32 && - kotlin.math.abs(bounds.centerY() - target.bounds.centerY) <= 32 - ) matches += node + val candidates = mutableListOf() + val liveNodes = mutableMapOf() + fun visit(node: AccessibilityNodeInfo, path: String) { + if (node.isVisibleToUser && node.isEnabled && node.isScrollable) { + val bounds = Rect().also(node::getBoundsInScreen) + candidates += PurchaseScrollCandidate( + path, node.className?.toString(), + NodeBounds(bounds.left, bounds.top, bounds.right, bounds.bottom), + ) + liveNodes[path] = node + } + for (index in 0 until node.childCount) { + node.getChild(index)?.let { visit(it, "$path/$index") } + } } - if (matches.size != 1) return false - return swipeNode(matches.single(), direction, durationMs, preferScrollAction = true) + visit(root, "0") + val resolved = PurchaseScrollLocator.locate( + PurchaseScrollCandidate(target.path, target.className, target.bounds), candidates, + ) ?: return false + val current = liveNodes[resolved.path] ?: return false + return swipeNode(current, direction, durationMs, preferScrollAction = true) } override fun backPurchase(): Boolean = performGlobalAction(GLOBAL_ACTION_BACK) diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseScrollLocator.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseScrollLocator.kt new file mode 100644 index 0000000..17b7b1b --- /dev/null +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseScrollLocator.kt @@ -0,0 +1,24 @@ +package cn.ilapage.goauto.agent.automation + +internal data class PurchaseScrollCandidate( + val path: String, + val className: String?, + val bounds: NodeBounds, +) + +internal object PurchaseScrollLocator { + fun locate(target: PurchaseScrollCandidate, candidates: List): PurchaseScrollCandidate? { + val compatible = candidates.filter { + it.className == target.className && boundsMatch(it.bounds, target.bounds) + } + val samePath = compatible.filter { it.path == target.path } + if (samePath.isNotEmpty()) return samePath.singleOrNull() + return compatible.singleOrNull() + } + + private fun boundsMatch(a: NodeBounds, b: NodeBounds): Boolean = + kotlin.math.abs(a.left - b.left) <= 32 && + kotlin.math.abs(a.top - b.top) <= 32 && + kotlin.math.abs(a.right - b.right) <= 32 && + kotlin.math.abs(a.bottom - b.bottom) <= 32 +} diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseScrollLocatorTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseScrollLocatorTest.kt new file mode 100644 index 0000000..479a8a0 --- /dev/null +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseScrollLocatorTest.kt @@ -0,0 +1,39 @@ +package cn.ilapage.goauto.agent + +import cn.ilapage.goauto.agent.automation.NodeBounds +import cn.ilapage.goauto.agent.automation.PurchaseScrollCandidate +import cn.ilapage.goauto.agent.automation.PurchaseScrollLocator +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class PurchaseScrollLocatorTest { + private val outer = PurchaseScrollCandidate("0/1", "RecyclerView", NodeBounds(0, 1038, 1080, 2079)) + private val inner = PurchaseScrollCandidate("0/1/0", "RecyclerView", NodeBounds(36, 1149, 1080, 2007)) + + @Test fun nestedContainersWithNearbyCentersResolveOuter() { + assertEquals(outer, PurchaseScrollLocator.locate(outer, listOf(inner, outer))) + } + + @Test fun pathDisambiguatesIdenticalBounds() { + val nested = outer.copy(path = "0/1/0") + assertEquals(outer, PurchaseScrollLocator.locate(outer, listOf(nested, outer))) + } + + @Test fun changedPathRequiresUniqueFullBoundsMatch() { + val moved = outer.copy(path = "0/2") + assertEquals(moved, PurchaseScrollLocator.locate(outer, listOf(inner, moved))) + assertNull(PurchaseScrollLocator.locate(outer, listOf(moved, moved.copy(path = "0/3")))) + } + + @Test fun reusedPathWithDifferentGeometryIsRejected() { + assertNull(PurchaseScrollLocator.locate(outer, listOf(inner.copy(path = outer.path)))) + } + + @Test fun classAndAllEdgesAreValidated() { + assertNull(PurchaseScrollLocator.locate(outer, listOf(outer.copy(className = "ScrollView")))) + assertNull(PurchaseScrollLocator.locate(outer, listOf( + outer.copy(bounds = NodeBounds(0, 1138, 1080, 1979)), + ))) + } +}