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") } // WaitForChange waits until the screen no longer matches before, and returns // the new signature. // // The caller captures before with ScreenSignature *before* the input it wants to // judge. That ordering is the whole check: an app that has stopped consuming // input still draws the screen it had, so the signature after the input is // identical to the one before it. A dump is not instant, so a baseline taken // after the input would already include the app's response and could never see // the wedge. func WaitForChange(r *adb.Runner, before string, timeout time.Duration) (string, error) { if strings.TrimSpace(before) == "" { return "", fmt.Errorf("no screen signature was captured before the input") } if timeout <= 0 { timeout = 10 * time.Second } deadline := time.Now().Add(timeout) for { sig, err := ScreenSignature(r) if err != nil { return "", err } if sig != before { return sig, nil } if time.Now().After(deadline) { return before, fmt.Errorf("the screen did not change within %s", timeout) } time.Sleep(PollInterval) } }