fix(agent): distinguish nested purchase scroll containers (#230)
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
+20
-10
@@ -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<AccessibilityNodeInfo>()
|
||||
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<PurchaseScrollCandidate>()
|
||||
val liveNodes = mutableMapOf<String, AccessibilityNodeInfo>()
|
||||
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)
|
||||
|
||||
@@ -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>): 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
|
||||
}
|
||||
@@ -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)),
|
||||
)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user