# proof_common.ps1 - shared device-side proof helpers (4b, personal docs loop-4b S6). # # ASCII-only file (PS 5.1 reads BOM-less .ps1 as GBK; repo lessons). # Dot-source it: . "$PSScriptRoot\proof_common.ps1" # # Provenance: these are the battle-tested helpers from landscape_proof.ps1 (4a: # wake-only-from-proven-hidden, positive-pair hide check, N25-style unique temp # files, uiautomator empty-dump retry) plus rel_mouse_phone_proof's focus-assert # field clearing. Older proofs are NOT migrated to this file in loop 4b (changing # four validated scripts without re-running all four is itself a false-green # risk) - migration is registered as a harness hygiene item for the next loop. # ---- adb wrappers (never let stderr trip ErrorActionPreference=Stop) ---- if (-not (Get-Variable -Name PccAdb -Scope Global -ErrorAction SilentlyContinue)) { $global:PccAdb = "D:\working\android\sdk\platform-tools\adb.exe" } function Adb { param([Parameter(Mandatory)][string[]]$AdbArgs) $old = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { $out = & $global:PccAdb @AdbArgs 2>$null @{ out = ($out -join "`n"); code = $LASTEXITCODE } } finally { $ErrorActionPreference = $old } } function AdbShell { param([string[]]$Cmd) (Adb -AdbArgs (@("shell") + $Cmd)).out } # ---- UI tree ---- # Unique temp per process (N25 lesson applied to dump pulls, cheap insurance). $script:PccUiTmp = "$env:TEMP\pcc_ui_$PID.xml" $script:PccDevDump = "/sdcard/pcc_$PID.xml" function Get-Ui { [void](AdbShell @("uiautomator", "dump", $script:PccDevDump)) $r = Adb -AdbArgs @("pull", $script:PccDevDump, $script:PccUiTmp) if ($r.code -ne 0 -or -not (Test-Path $script:PccUiTmp)) { return "" } return [System.IO.File]::ReadAllText($script:PccUiTmp, [System.Text.Encoding]::UTF8) } # uiautomator intermittently dumps empty (measured repeatedly in 4a). Retry until # the tree is plausibly real. Callers still assert on their own node - this only # removes the transient, never masks a genuine absence. function Get-UiRetry([int]$tries = 4) { $x = "" for ($i = 0; $i -lt $tries; $i++) { $x = Get-Ui if ($x.Length -gt 500) { return $x } Start-Sleep -Milliseconds 400 } Write-Host " [ui] retry exhausted, last dump len=$($x.Length)" return $x } function VisibleTexts([string]$xml) { [regex]::Matches($xml, 'text="([^"]+)"') | ForEach-Object { [System.Net.WebUtility]::HtmlDecode($_.Groups[1].Value) } } # ---- buttons: match by LABEL, never by position (3b/N23 lesson) ---- function ButtonNode([string]$xml, [string]$want, [bool]$prefix = $false) { foreach ($n in [regex]::Matches($xml, ']*class="android\.widget\.Button"[^>]*>')) { $t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value, 'text="([^"]*)"').Groups[1].Value) $hit = if ($prefix) { $t.StartsWith($want) } else { $t -eq $want } if ($hit) { return $n.Value } } return $null } function ButtonCenter([string]$xml, [string]$want, [bool]$prefix = $false) { $v = ButtonNode $xml $want $prefix if (-not $v) { return $null } $b = [regex]::Match($v, 'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') if (-not $b.Success) { return $null } @{ cx = [int](([int]$b.Groups[1].Value + [int]$b.Groups[3].Value) / 2) cy = [int](([int]$b.Groups[2].Value + [int]$b.Groups[4].Value) / 2) } } function ButtonLabelByPrefix([string]$xml, [string]$prefix) { foreach ($n in [regex]::Matches($xml, ']*class="android\.widget\.Button"[^>]*>')) { $t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value, 'text="([^"]*)"').Groups[1].Value) if ($t.StartsWith($prefix)) { return $t } } return $null } # ---- 4a observation-surface discipline: controls auto-hide in landscape ---- # Hidden state = POSITIVE PAIR (video View node present AND zero Button nodes). # An empty dump would otherwise pass "no buttons" trivially (N24 false-fails). function ControlsHidden([string]$ui) { $anyBtn = ($ui -match 'class="android\.widget\.Button"') $video = ($ui -match 'class="android\.view\.View"') return ($video -and -not $anyBtn) } # Wake tap lives in the letterbox black bar (x=100): outside the video in both # states, consumed by the app only when hidden - zero remote leakage (4a/D1). function Wake-Band { $d = LocalDisplay [void](AdbShell @("input", "tap", "100", "$([int]($d.h / 2))")) Start-Sleep -Milliseconds 400 } function Find-VisibleButton([string]$want, [bool]$prefix = $false) { for ($try = 0; $try -lt 5; $try++) { $ui = Get-UiRetry $b = ButtonCenter $ui $want $prefix if ($b) { return $b } if (ControlsHidden $ui) { Wake-Band; continue } if ($ui.Length -gt 500) { return $null } # real tree, genuine absence Start-Sleep -Milliseconds 400 # empty-dump transient } return $null } function Tap-VisibleButton([string]$want, [bool]$prefix = $false) { $b = Find-VisibleButton $want $prefix if (-not $b) { return $null } [void](AdbShell @("input", "tap", "$($b.cx)", "$($b.cy)")) return $b } function Read-ButtonLabel([string]$want, [bool]$prefix = $false) { for ($try = 0; $try -lt 6; $try++) { $ui = Get-UiRetry $l = ButtonLabelByPrefix $ui $want if ($l) { return $l } if (ControlsHidden $ui) { Wake-Band; continue } if ($ui.Length -gt 500) { return $null } Start-Sleep -Milliseconds 400 } return $null } # ---- HUD / session liveness ---- # "zhen=" frame counter (char U+5E27 as code point, ASCII-safe source). function FrameCount { $m = [regex]::Match((Get-Ui), ([char]0x5E27 + '=(\d+)')) if ($m.Success) { return [long]$m.Groups[1].Value } return -1 } function WakeControls { $d = LocalDisplay [void](AdbShell @("input", "tap", "100", "$([int]($d.h / 2))")) Start-Sleep -Milliseconds 500 } function FrameCountOrWake { $f = FrameCount if ($f -ge 0) { return $f } WakeControls return FrameCount } # Live HUD line (RTT= only exists when connected; hidden-controls caveat above). function Wait-LiveHud([int]$seconds = 20) { $deadline = (Get-Date).AddSeconds($seconds) while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500 $ui = Get-Ui if ($ui -notmatch "RTT=") { if (ControlsHidden $ui) { WakeControls } continue } $m = [regex]::Match($ui, 'text="([^"]*RTT=[^"]*)"') if ($m.Success) { return [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value) } } return $null } # ---- display / app lifecycle ---- # vivo ROM prints NO mRotation= line and keeps a permanent 1080x2400 virtual # display (N30): orientation MUST be read from cur= inside the mDisplayId=0 block. function LocalDisplay { $d = (AdbShell @("dumpsys", "window", "displays")) $idx = $d.IndexOf("Display: mDisplayId=0") if ($idx -lt 0) { return @{ w = 0; h = 0 } } $m = [regex]::Match($d.Substring($idx), 'cur=(\d+)x(\d+)') if (-not $m.Success) { return @{ w = 0; h = 0 } } @{ w = [int]$m.Groups[1].Value; h = [int]$m.Groups[2].Value } } function Wake { [void](AdbShell @("input", "keyevent", "224")) Start-Sleep -Milliseconds 250 [void](AdbShell @("wm", "dismiss-keyguard")) Start-Sleep -Milliseconds 350 } function Restart-AppForm { Wake [void](AdbShell @("am", "force-stop", "com.verify.remote")) Start-Sleep -Milliseconds 600 [void](AdbShell @("am", "start", "-n", "com.verify.remote/.MainActivity")) $deadline = (Get-Date).AddSeconds(8) while ((Get-Date) -lt $deadline) { $x = Get-Ui if (([regex]::Matches($x, 'class="android\.widget\.EditText"')).Count -ge 3) { return $x } Start-Sleep -Milliseconds 300 } throw "form did not appear: [$(VisibleTexts (Get-Ui) -join ' || ')]" } # ---- connect form: focus-assert typing (rel_mouse_proof grade, R-8) ---- function CenterOf($m) { @([int](([int]$m.Groups[1].Value + [int]$m.Groups[3].Value) / 2), [int](([int]$m.Groups[2].Value + [int]$m.Groups[4].Value) / 2)) } function FormFields([string]$xml) { [regex]::Matches($xml, ']*class="android\.widget\.EditText"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') } function TypeAscii([string]$s) { foreach ($ch in $s.ToUpper().ToCharArray()) { $k = if ($ch -ge '0' -and $ch -le '9') { 7 + [int][string]$ch } elseif ($ch -ge 'A' -and $ch -le 'Z') { 29 + ([int]$ch - [int][char]'A') } elseif ($ch -eq '.') { 56 } else { throw "unsupported char $ch" } [void](AdbShell @("input", "keyevent", "$k")) Start-Sleep -Milliseconds 70 } } function FocusedIndex([string]$xml) { $i = -1 foreach ($n in [regex]::Matches($xml, ']*>')) { if ($n.Value -match 'class="android\.widget\.EditText"') { $i++ if ($n.Value -match 'focused="true"') { return $i } } } return -1 } function Clear-Field($c, [int]$wantIdx) { for ($try = 0; $try -lt 3; $try++) { [void](AdbShell @("input", "tap", "$($c[0])", "$($c[1])")) $deadline = (Get-Date).AddSeconds(1.5) while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 250 if ((FocusedIndex (Get-Ui)) -eq $wantIdx) { for ($i = 0; $i -lt 40; $i++) { [void](AdbShell @("input", "keyevent", "67")) } Start-Sleep -Milliseconds 120 return } } } throw "field $wantIdx never took focus" } function FillAndConnect([string]$xml, [string]$host_, [string]$port_, [string]$code_, [string]$fConnect, [string]$fReconnect) { $f = FormFields $xml if ($f.Count -lt 3) { throw "form has $($f.Count) fields" } Clear-Field (CenterOf $f[0]) 0; TypeAscii $host_ Clear-Field (CenterOf $f[1]) 1; TypeAscii $port_ Clear-Field (CenterOf $f[2]) 2; TypeAscii $code_ $expect = @($host_, $port_, $code_) $vals = @() for ($retry = 0; $retry -le 2; $retry++) { $x2 = Get-UiRetry $vals = @() foreach ($n in [regex]::Matches($x2, ']*>')) { if ($n.Value -match 'class="android\.widget\.EditText"') { $vals += [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value, 'text="([^"]*)"').Groups[1].Value) } } if ($vals.Count -lt 3) { throw "expected 3 fields after typing, got $($vals.Count)" } $bad = -1 for ($i = 0; $i -lt 3; $i++) { if ($vals[$i] -ne $expect[$i]) { $bad = $i; break } } if ($bad -ge 0) { $f = FormFields $x2 Clear-Field (CenterOf $f[$bad]) $bad TypeAscii $expect[$bad] continue } $btn = ButtonCenter $x2 $fConnect if (-not $btn) { $btn = ButtonCenter $x2 $fReconnect } if (-not $btn) { $btn = Find-VisibleButton $fConnect if (-not $btn) { $btn = Find-VisibleButton $fReconnect } } if (-not $btn) { throw "connect button not found" } [void](AdbShell @("input", "tap", "$($btn.cx)", "$($btn.cy)")) return } throw "form input never converged: [$($vals -join ' | ')]" } # ---- video geometry (uiautomator has no SurfaceView - match 16:9 View, N21) ---- function VideoBounds([string]$xml) { $best = $null; $bestErr = 1e9 foreach ($n in [regex]::Matches($xml, ']*class="android\.view\.View"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')) { $x1 = [int]$n.Groups[1].Value; $y1 = [int]$n.Groups[2].Value $x2 = [int]$n.Groups[3].Value; $y2 = [int]$n.Groups[4].Value $w = $x2 - $x1; $h = $y2 - $y1 if ($w -lt 300 -or $h -lt 100) { continue } $err = [Math]::Abs($w / [double]$h - 16.0 / 9.0) if ($err -lt $bestErr) { $bestErr = $err; $best = @{ l = $x1; t = $y1; w = $w; h = $h } } } if ($best) { $best.err = $bestErr } return $best } # ---- PC-side cursor: DPI-aware set with hijack gates ---- if (-not ('PccCur' -as [type])) { Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; public class PccCur { [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; } [DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT p); [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y); [DllImport("user32.dll")] public static extern bool SetProcessDpiAwarenessContext(IntPtr c); public static void Dpi(){ SetProcessDpiAwarenessContext(new IntPtr(-4)); } public static int[] Get(){ POINT p; GetCursorPos(out p); return new int[]{p.X,p.Y}; } public static void Set(int x,int y){ SetCursorPos(x,y); } } "@ } [PccCur]::Dpi() # SetCursorQuiet: the machine may be shared (2026-11-02 probe: a human used the # real mouse mid-probe and faked every reading). Gate: two reads 350ms apart must # agree (nobody is moving), then Set + read-back must equal the request; retries # on hijack; loud failure instead of a silently wrong sample (R-8). function Set-CursorQuiet([int]$x, [int]$y, [int]$tries = 8) { for ($i = 0; $i -lt $tries; $i++) { $a = [PccCur]::Get() Start-Sleep -Milliseconds 350 $b = [PccCur]::Get() if (-not ($a[0] -eq $b[0] -and $a[1] -eq $b[1])) { Write-Host " [cursor] busy ($($a -join ',')->$($b -join ',')), retry $($i+1)" Start-Sleep -Seconds 1 continue } [PccCur]::Set($x, $y) Start-Sleep -Milliseconds 120 $g = [PccCur]::Get() if ($g[0] -eq $x -and $g[1] -eq $y) { return } Write-Host " [cursor] readback ($($g -join ',')) != ($x,$y), retry $($i+1)" Start-Sleep -Milliseconds 500 } throw "cursor never settled at ($x,$y) - someone is using the real mouse; retry the proof when the PC is idle" } # ---- memory precheck (N20/N29: OOM fakes build/test failures) ---- function Memory-Precheck([double]$minGB = 0.5) { $os = Get-CimInstance Win32_OperatingSystem $freeGB = $os.FreeVirtualMemory / 1MB Write-Host (" commit free: {0:N1} GB" -f $freeGB) if ($freeGB -lt $minGB) { throw "commit memory critically low - free RAM first (gradle --stop)" } } # ---- R-7 stale-agent guard: binary newer than process => restart ---- function Ensure-FreshAgent([string]$AgentExe, [int]$Port, [string]$Code) { if (-not (Test-Path $AgentExe)) { throw "missing $AgentExe (cargo build --release -p agent)" } $ag = @(Get-Process agent -ErrorAction SilentlyContinue) if ($ag.Count -gt 0 -and ((Get-Item $AgentExe).LastWriteTime -gt $ag[0].StartTime)) { Write-Host " agent predates build; restarting" Stop-Process -Name agent -Force -ErrorAction SilentlyContinue Start-Sleep -Milliseconds 800; $ag = @() } if ($ag.Count -eq 0) { Start-Process $AgentExe -ArgumentList "--port", "$Port", "--code", $Code -WindowStyle Hidden | Out-Null Start-Sleep -Seconds 2 Write-Host " agent started (fresh)" } else { Write-Host " agent running and current" } }