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) } }