An app can stop consuming input while it keeps drawing: Android delivers the tap, the app never acts on it. Phase 3 hit this three times against Spotify and the recipe failed on the label it was waiting for, which names the wrong problem. The new `expect_change <duration>` step fingerprints the screen (foreground package plus every visible label and its bounds) and fails when the screen does not change after the previous step, with the distinct reason "the app stopped consuming input". It supports `retry N`, which re-presses the step above a bounded number of times, so a dropped tap is still recovered and a wedged app still ends the run (FR-11). Co-Authored-By: Paperclip <noreply@paperclip.ing>
85 lines
2.7 KiB
Go
85 lines
2.7 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.lab.audasmedia.com.au/sam/artemis_control/internal/adb"
|
|
)
|
|
|
|
// ScreenSignature returns a fingerprint of what is on screen: the foreground
|
|
// package plus every visible label and the bounds it sits in.
|
|
//
|
|
// Two dumps of an unchanged screen produce the same signature, and any change a
|
|
// human would see — new text, different text, an element moved or a different
|
|
// app in front — produces a different one. It is the raw material for wedge
|
|
// detection: if an input is delivered and the signature never moves, the app
|
|
// never acted on that input.
|
|
//
|
|
// The foreground package is best effort. If it cannot be read the labels alone
|
|
// still make a usable fingerprint, because a screen that stops changing is
|
|
// exactly the failure this guards against.
|
|
func ScreenSignature(r *adb.Runner) (string, error) {
|
|
h, err := Dump(r)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fg, _ := ForegroundPackage(r)
|
|
return h.Signature(fg), nil
|
|
}
|
|
|
|
// Signature is the pure half of ScreenSignature: a stable fingerprint of one
|
|
// parsed dump. Labels are sorted so that the order uiautomator happened to
|
|
// report them in never looks like a change, and bounds are included so that an
|
|
// element moving is a change even when the words stay the same.
|
|
func (h *Hierarchy) Signature(foreground string) string {
|
|
parts := make([]string, 0, len(h.TextNodes()))
|
|
h.Walk(func(n *Node) {
|
|
label := strings.TrimSpace(n.Text)
|
|
if label == "" {
|
|
label = strings.TrimSpace(n.ContentDesc)
|
|
}
|
|
if label == "" {
|
|
return
|
|
}
|
|
parts = append(parts, strings.TrimSpace(n.Bounds)+"|"+label)
|
|
})
|
|
sort.Strings(parts)
|
|
return foreground + "\n" + strings.Join(parts, "\n")
|
|
}
|
|
|
|
// ExpectChange waits until the screen no longer looks like it did when the
|
|
// function was called, and returns the signatures before and after.
|
|
//
|
|
// This is the wedged-app check. An app that has stopped consuming input still
|
|
// draws the screen it had, so a delivered tap changes nothing. A timeout here
|
|
// means the last input was accepted by Android but never acted on by the app,
|
|
// which the recipe reports as "the app stopped consuming input" instead of
|
|
// blaming the label it was waiting for (AGENTS.md section 11 Phase 5).
|
|
func ExpectChange(r *adb.Runner, timeout time.Duration) (before, after string, err error) {
|
|
if timeout <= 0 {
|
|
timeout = 10 * time.Second
|
|
}
|
|
before, err = ScreenSignature(r)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
deadline := time.Now().Add(timeout)
|
|
for {
|
|
sig, derr := ScreenSignature(r)
|
|
if derr != nil {
|
|
return before, "", derr
|
|
}
|
|
if sig != before {
|
|
return before, sig, nil
|
|
}
|
|
if !time.Now().Before(deadline) {
|
|
return before, before, fmt.Errorf("the screen did not change within %s", timeout)
|
|
}
|
|
time.Sleep(PollInterval)
|
|
}
|
|
}
|