fix(android): exit photo search layer before returning home #250
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "cn.ilapage.goauto.agent"
|
||||
minSdk = 23
|
||||
targetSdk = 34
|
||||
versionCode = 83
|
||||
versionName = "0.9.70"
|
||||
versionCode = 84
|
||||
versionName = "0.9.71"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
+42
-14
@@ -5,31 +5,50 @@ internal class PddCollectionHomeNavigator(
|
||||
private val driver: PddCollectorDriver,
|
||||
private val pause: (Long) -> Unit = Thread::sleep,
|
||||
) {
|
||||
var reason: String = "not_started"
|
||||
private set
|
||||
var actions: Int = 0
|
||||
private set
|
||||
|
||||
private fun finish(value: String, success: Boolean = false): Boolean {
|
||||
reason = value
|
||||
return success
|
||||
}
|
||||
|
||||
fun returnHome(rule: CollectionRule): Boolean = runCatching {
|
||||
actions = 0
|
||||
var screen = driver.capture()
|
||||
repeat(MAX_ACTIONS) {
|
||||
if (!safe(screen)) return false
|
||||
if (isHome(screen)) return true
|
||||
if (!safe(screen)) return finish("unsafe_page")
|
||||
if (isHome(screen)) return finish("home_confirmed", true)
|
||||
val before = signature(screen)
|
||||
val home = homeTab(screen)
|
||||
val photoSearch = isPhotoSearchLayer(screen)
|
||||
val home = if (photoSearch) null else homeTab(screen)
|
||||
actions++
|
||||
if (home != null) {
|
||||
if (driver.clickFresh(home) != FreshActionResult.SUCCESS) return false
|
||||
if (driver.clickFresh(home) != FreshActionResult.SUCCESS) return finish("home_click_failed")
|
||||
} else {
|
||||
val parsed = rule.collector?.let { PddScreenParser.parse(screen, it, "", rule.pageEvidence) }
|
||||
val knownProduct = parsed != null && parsed.pageEvidenceMatched &&
|
||||
(parsed.specPanelOpen || parsed.specEntry != null)
|
||||
if (!knownProduct && topBack(screen) == null) return false
|
||||
if (!driver.back()) return false
|
||||
if (!photoSearch && !knownProduct && topBack(screen) == null) {
|
||||
actions--
|
||||
return finish("unknown_page")
|
||||
}
|
||||
if (!driver.back()) return finish("back_failed")
|
||||
}
|
||||
// Read after each individual action; never send a sequence of blind Back events.
|
||||
pause(500)
|
||||
screen = driver.capture()
|
||||
if (!safe(screen)) return false
|
||||
if (isHome(screen)) return true
|
||||
if (signature(screen) == before) return false
|
||||
for (read in 1..3) {
|
||||
pause(500)
|
||||
screen = driver.capture()
|
||||
if (!safe(screen)) return finish("unsafe_page")
|
||||
if (isHome(screen)) return finish("home_confirmed", true)
|
||||
if (signature(screen) != before) break
|
||||
}
|
||||
if (signature(screen) == before) return finish(if (photoSearch) "search_back_unchanged" else "page_unchanged")
|
||||
}
|
||||
false
|
||||
}.getOrDefault(false)
|
||||
finish("action_limit")
|
||||
}.getOrElse { finish("navigation_exception") }
|
||||
|
||||
private fun topBack(screen: UiSnapshot): SnapshotNode? {
|
||||
val height = screen.nodes.maxOfOrNull { it.bounds.bottom } ?: return null
|
||||
@@ -44,6 +63,15 @@ internal class PddCollectionHomeNavigator(
|
||||
const val MAX_ACTIONS = 4
|
||||
private const val PDD = "com.xunmeng.pinduoduo"
|
||||
|
||||
internal fun isPhotoSearchLayer(screen: UiSnapshot): Boolean {
|
||||
val nodes = screen.nodes.filter { it.visible && it.enabled }
|
||||
val height = nodes.maxOfOrNull { it.bounds.bottom } ?: return false
|
||||
val width = nodes.maxOfOrNull { it.bounds.right } ?: return false
|
||||
if (height <= 0 || width <= 0) return false
|
||||
return nodes.any { it.label == "拍照搜索" && it.className == "android.widget.FrameLayout" &&
|
||||
!it.clickable && it.bounds.height >= height * 4 / 5 && it.bounds.width >= width * 4 / 5 }
|
||||
}
|
||||
|
||||
internal fun safe(screen: UiSnapshot): Boolean = screen.packageName == PDD &&
|
||||
!screen.activityName.isNullOrBlank() &&
|
||||
PddPageClassifier.classify(screen.packageName, screen.activityName, screen.nodes.filter { it.visible }.map { it.label }) == null &&
|
||||
@@ -63,7 +91,7 @@ internal class PddCollectionHomeNavigator(
|
||||
}
|
||||
|
||||
internal fun isHome(screen: UiSnapshot): Boolean {
|
||||
if (!safe(screen)) return false
|
||||
if (!safe(screen) || isPhotoSearchLayer(screen)) return false
|
||||
val home = homeTab(screen) ?: return false
|
||||
val height = screen.nodes.maxOfOrNull { it.bounds.bottom } ?: return false
|
||||
return home.selected && screen.nodes.any { it.visible && it.enabled && it.clickable &&
|
||||
|
||||
@@ -667,11 +667,12 @@ class AgentForegroundService : Service() {
|
||||
cancelIdleReturn("连续采集保留 PDD 页面")
|
||||
ContinuousCollectionPolicy.finish(continuous, outcome) {
|
||||
val driver = GoAutoAccessibilityService.instance ?: return@finish false
|
||||
val home = PddCollectionHomeNavigator(driver).returnHome(RuleParser.parse(initialTask.ruleSnapshot))
|
||||
val navigator = PddCollectionHomeNavigator(driver)
|
||||
val home = navigator.returnHome(RuleParser.parse(initialTask.ruleSnapshot))
|
||||
runCatching {
|
||||
val hash = java.security.MessageDigest.getInstance("SHA-256")
|
||||
.digest(initialTask.ruleSnapshot.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it) }
|
||||
Log.i("GoAutoCollector", "task=${initialTask.taskId};attempt=${initialTask.attemptNumber};device=${stateStore.read().deviceId};rule=$hash;continuousHome=$home")
|
||||
Log.i("GoAutoCollector", "task=${initialTask.taskId};attempt=${initialTask.attemptNumber};device=${stateStore.read().deviceId};rule=$hash;continuousHome=$home;reason=${navigator.reason};actions=${navigator.actions}")
|
||||
}
|
||||
home
|
||||
}.also { result ->
|
||||
|
||||
@@ -109,6 +109,60 @@ class ContinuousCollectionTest {
|
||||
assertFalse(PddCollectionHomeNavigator(failure, {}).returnHome(rule))
|
||||
}
|
||||
|
||||
@Test fun `photo search layer takes priority over underlying home tabs`() {
|
||||
val search = photoSearch(home())
|
||||
assertFalse(PddCollectionHomeNavigator.isHome(search))
|
||||
val d = Driver(listOf(search, home(selected = false), home()))
|
||||
val navigator = PddCollectionHomeNavigator(d, {})
|
||||
assertTrue(navigator.returnHome(rule))
|
||||
assertEquals(listOf("capture", "back", "capture", "click", "capture"), d.calls)
|
||||
assertEquals("home_confirmed", navigator.reason)
|
||||
assertEquals(2, navigator.actions)
|
||||
}
|
||||
|
||||
@Test fun `search without labelled back returns but ordinary camera entry does not`() {
|
||||
val d = Driver(listOf(photoSearch(page()), home()))
|
||||
assertTrue(PddCollectionHomeNavigator(d, {}).returnHome(rule))
|
||||
assertEquals(1, d.backs)
|
||||
assertFalse(PddCollectionHomeNavigator.isPhotoSearchLayer(home()))
|
||||
}
|
||||
|
||||
@Test fun `unchanged search waits bounded time without repeated back`() {
|
||||
val d = Driver(listOf(photoSearch(page())))
|
||||
val waits = mutableListOf<Long>()
|
||||
val navigator = PddCollectionHomeNavigator(d, { waits += it })
|
||||
assertFalse(navigator.returnHome(rule))
|
||||
assertEquals("search_back_unchanged", navigator.reason)
|
||||
assertEquals(1, d.backs)
|
||||
assertEquals(listOf(500L, 500L, 500L), waits)
|
||||
}
|
||||
|
||||
@Test fun `delayed search exit is read again without another navigation action`() {
|
||||
val search = photoSearch(page())
|
||||
val d = Driver(listOf(search, search, home())).apply { advanceOnCapture = true }
|
||||
val navigator = PddCollectionHomeNavigator(d, {})
|
||||
assertTrue(navigator.returnHome(rule))
|
||||
assertEquals(1, d.backs)
|
||||
assertEquals(listOf("capture", "back", "capture", "capture"), d.calls)
|
||||
}
|
||||
|
||||
@Test fun `search overlay never bypasses risky page or foreground guard`() {
|
||||
for (screen in listOf(photoSearch(page(node("risk", "订单详情"))),
|
||||
photoSearch(page()).copy(packageName = "other"))) {
|
||||
val d = Driver(listOf(screen))
|
||||
val navigator = PddCollectionHomeNavigator(d, {})
|
||||
assertFalse(navigator.returnHome(rule))
|
||||
assertEquals("unsafe_page", navigator.reason)
|
||||
assertEquals(0, d.backs + d.clicks)
|
||||
}
|
||||
val d = Driver(listOf(photoSearch(page()), home().copy(packageName = "other")))
|
||||
assertFalse(PddCollectionHomeNavigator(d, {}).returnHome(rule))
|
||||
assertEquals(1, d.backs)
|
||||
}
|
||||
|
||||
private fun photoSearch(base: UiSnapshot) = base.copy(nodes = base.nodes +
|
||||
node("search-layer", "拍照搜索").copy(className = "android.widget.FrameLayout", bounds = NodeBounds(0, 0, 1080, 2354)))
|
||||
|
||||
private fun node(path: String, label: String, y: Int = 30, selected: Boolean = false, clickable: Boolean = false, parent: String? = null) =
|
||||
SnapshotNode(path, parent, label, null, null, "android.widget.TextView", NodeBounds(20, y, 120, y + 40),
|
||||
clickable, false, selected, false, true, true)
|
||||
@@ -130,12 +184,17 @@ class ContinuousCollectionTest {
|
||||
var backs = 0
|
||||
var clicks = 0
|
||||
var throwOnBack = false
|
||||
var advanceOnCapture = false
|
||||
val calls = mutableListOf<String>()
|
||||
override fun capture(): UiSnapshot { calls += "capture"; return screens[index.coerceAtMost(screens.lastIndex)] }
|
||||
override fun capture(): UiSnapshot {
|
||||
calls += "capture"
|
||||
return screens[index.coerceAtMost(screens.lastIndex)].also { if (advanceOnCapture) index++ }
|
||||
}
|
||||
override fun back(): Boolean {
|
||||
calls += "back"; backs++
|
||||
if (throwOnBack) error("simulated")
|
||||
index++; return true
|
||||
if (!advanceOnCapture) index++
|
||||
return true
|
||||
}
|
||||
override fun clickFresh(target: SnapshotNode): FreshActionResult { calls += "click"; clicks++; index++; return FreshActionResult.SUCCESS }
|
||||
override fun swipeSpec(direction: SwipeDirection, anchor: SnapshotNode?) = error("unexpected swipe")
|
||||
|
||||
Reference in New Issue
Block a user