# 3a connect-lifecycle proof (phone side): U8 timeout/cancel, error classes, BACK-cancel, HUD. # # ASCII-only source (PS 5.1 decodes BOM-less .ps1 as GBK; see personal lessons). # Expected Chinese UI strings are matched as CODE-POINT fragments extracted from # the Kotlin sources at authoring time (R-8: assert the actual value, and print # what the parser really read so an empty result cannot pass silently). # # Judgements (V-A1 rewritten per loop-3-plan D3 redial ruling, user 2026-10-31): # V-A2a wrong code -> AUTH class text + original-error detail line # V-A1a unresolvable host -> RESOLVE text fast (<3s; Resolve is NOT retryable) # V-A1b blackhole IP -> connecting shows CANCEL; tap -> actionable form <1.5s # V-A4 BACK while connecting -> cancel to form, activity NOT finished # V-A5 live session -> HUD has fps= and no coordinate noise (norm=/view=) # # The IME is disabled for the whole run (form fields then receive raw key events, # no soft keyboard covering the button; same pattern as phone_chain_proof step 6) # and restored in `finally`. # # Prereqs: vivo device online via adb, 3a APK installed, v3 agent build present. # powershell -ExecutionPolicy Bypass -File tools\connect_lifecycle_proof.ps1 -Host_ 192.168.1.110 param( [string]$Host_ = "192.168.1.110", [int]$Port = 47800, [string]$Code = "t", [string]$AgentExe = "" ) $ErrorActionPreference = "Stop" $repo = (Resolve-Path "$PSScriptRoot\..").Path $adb = "D:\working\android\sdk\platform-tools\adb.exe" $ev = Join-Path (Split-Path $repo -Parent) "evidence\3a" New-Item -ItemType Directory -Force -Path $ev | Out-Null # ---- code-point fragments extracted from Kotlin sources THIS authoring session # (grep-verified present in ConnectErrorText.kt / MainActivity.kt): ---- # NB: build with `-join (...)`, NOT `[string](a,b)` - the latter stringifies the # array with spaces ("连 接", len 3) and silently never matches (measured # 2026-10-31; visibleTexts comparison then finds 'no button' for a button that # is plainly on screen). $F_RESOLVE = -join ([char]0x65E0,[char]0x6CD5,[char]0x89E3,[char]0x6790,[char]0x8FD9,[char]0x4E2A,[char]0x5730,[char]0x5740) # "cannot resolve this address" $F_AUTH = -join ([char]0x9A8C,[char]0x8BC1,[char]0x7801,[char]0x9519,[char]0x8BEF) # "verification code wrong" $F_CANCELED = -join ([char]0x8FDE,[char]0x63A5,[char]0x5DF2,[char]0x53D6,[char]0x6D88) # "connection cancelled" $F_RECONNECT = -join ([char]0x91CD,[char]0x65B0,[char]0x8FDE,[char]0x63A5) # "reconnect" $F_CANCEL = -join ([char]0x53D6,[char]0x6D88) # "cancel" $F_CONNECT = -join ([char]0x8FDE,[char]0x63A5) # "connect" $F_KILLMSG = -join ([char]0x8FDE,[char]0x63A5,[char]0x5DF2,[char]0x65AD,[char]0x5F00) # "connection lost" function Adb { param([Parameter(Mandatory)][string[]]$AdbArgs) $old = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { $out = & $adb @AdbArgs 2>$null; @{ out = ($out -join "`n"); code = $LASTEXITCODE } } finally { $ErrorActionPreference = $old } } # NB: the concat must be parenthesized. `Adb @("shell") + $Cmd` parses `+` as a # literal extra argument (measured 2026-10-31: every shell call silently ran # `adb shell + ...` on the device, returning empty output - the form 'vanished' # because uiautomator never actually ran). function AdbShell { param([string[]]$Cmd) (Adb -AdbArgs (@("shell") + $Cmd)).out } function Get-UiText { $d = AdbShell @("uiautomator","dump","/sdcard/cl.xml") $r = Adb @("pull","/sdcard/cl.xml","$env:TEMP\cl.xml") if ($r.code -ne 0 -or -not (Test-Path "$env:TEMP\cl.xml")) { Write-Host " [ui] dump=[$($d -replace "`n",' ')] pull_rc=$($r.code) -> empty" return "" } $txt = [System.IO.File]::ReadAllText("$env:TEMP\cl.xml", [System.Text.Encoding]::UTF8) if ($txt.Length -lt 200) { # dump can 'succeed' yet hand back a stale stub; show what we really read Write-Host " [ui] short xml ($($txt.Length) chars): $($txt.Substring(0,[Math]::Min(120,$txt.Length)))" } return $txt } function VisibleTexts([string]$xml) { [regex]::Matches($xml,'text="([^"]+)"') | ForEach-Object { [System.Net.WebUtility]::HtmlDecode($_.Groups[1].Value) } } function FormFields([string]$xml) { [regex]::Matches($xml,']*class="android\.widget\.EditText"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') } function Center($m) { # NB: cast to [int] BEFORE adding. A regex group .Value is a STRING and PS # "60" + "1020" CONCATENATES ("601020") -> /2 then lands the tap ~300000px # off-screen, silently. Measured 2026-10-31; phone_chain_proof's Center is # the proven form - copy it, don't re-derive. $a=[int]$m.Groups[1].Value; $b=[int]$m.Groups[2].Value $c=[int]$m.Groups[3].Value; $d=[int]$m.Groups[4].Value @([int](($a+$c)/2), [int](($b+$d)/2)) } function ButtonCenter([string]$xml, [string]$want) { 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 -eq $want) { $b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') if ($b.Success) { $x1=[int]$b.Groups[1].Value; $y1=[int]$b.Groups[2].Value $x2=[int]$b.Groups[3].Value; $y2=[int]$b.Groups[4].Value return @([int](($x1+$x2)/2), [int](($y1+$y2)/2)) } } } return $null } # Physical-key input with the IME disabled. Key codes from javap against # android-34 (same table phone_chain_proof uses): KEYCODE_0=7..9=16, # KEYCODE_A=29..Z=54 contiguous (T=48 verified there), PERIOD=56. # NB1: [int]'A' converts the STRING and throws; letter anchor is [int][char]'A'. # NB2: [int][string]'0' == 0 (parse), so digits map 7+digit (an earlier draft # got this wrong twice: `-48`, then MINUS=14 - which is KEYCODE_7!). # MINUS (69) is deliberately NOT supported: throw instead of typing a wrong # key (measured 2026-10-31: a wrong MINUS mapping silently typed '7' into # the code field and corrupted the whole fill sequence). 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")) # 35ms lost a digit on the live device (port arrived as "4780"); # 70ms gap + per-field verify-and-retry below is the honest fix. Start-Sleep -Milliseconds 70 } } # which field (0..2) currently holds focus? "" if none. 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 } # Tap a field's center and VERIFY focus landed there (2b N-series discipline: # automation must assert its own preconditions - adb taps are not guaranteed). function Focus-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-UiText)) -eq $wantIdx) { return } } } throw "tap did not focus field $wantIdx after 3 tries (focused=$(FocusedIndex (Get-UiText)))" } function Clear-Field($c, [int]$idx) { Focus-Field $c $idx for ($i=0; $i -lt 40; $i++) { [void](AdbShell @("input","keyevent","67")) } Start-Sleep -Milliseconds 150 } function Restart-AppForm { # vivo keeps re-sleeping mid-run (measured 2026-10-31: battery-saver + a 7s # screen-off timeout re-locked the keyguard between steps, so uiautomator saw # the always-on-display clock instead of the app and 'form did not appear'). # Wake + dismiss is idempotent and keeps the harness unattended-safe; if the # device is PIN-locked a bare wake is not enough and this will fail loudly. [void](AdbShell @("input","keyevent","224")) # WAKEUP Start-Sleep -Milliseconds 300 [void](AdbShell @("wm","dismiss-keyguard")) Start-Sleep -Milliseconds 500 [void](AdbShell @("am","force-stop","com.verify.remote")) Start-Sleep -Milliseconds 600 [void](AdbShell @("am","start","-n","com.verify.remote/.MainActivity")) # no extras -> form $deadline = (Get-Date).AddSeconds(8) while ((Get-Date) -lt $deadline) { $x = Get-UiText if ((FormFields $x).Count -ge 3) { return $x } Start-Sleep -Milliseconds 300 } $diag = (VisibleTexts (Get-UiText)) -join " || " throw "connect form did not appear (expected >=3 EditText). screen shows: [$diag]" } # Fill host/port/code into the 3 form fields, asserting every precondition # (R-8 + 2026-10-31 lesson: an adb tap does NOT guarantee focus - one failed run # left all three type-ins in field 0 and the single 'vals[0] -ne host' check # caught it only as a corruption symptom). Now: focus is verified per field, and # every field's final text must equal its intended value. Returns tap time. function FillAndConnect([string]$xml, [string]$host_, [string]$port_, [string]$code_) { $f = FormFields $xml if ($f.Count -lt 3) { throw "form has $($f.Count) fields" } # NB: do NOT gather the three centers into @(a,b,c) - PowerShell array # literals FLATTEN nested arrays, silently turning 3 coord-pairs into 6 ints; # $cs[0] then holds only the x of field 0, every tap lands at a bogus point, # focus never moves (measured 2026-10-31: 'focused=-1' was the honest symptom). # Pass each call's array result directly as ONE argument. Clear-Field (Center $f[0]) 0; TypeAscii $host_ Clear-Field (Center $f[1]) 1; TypeAscii $port_ Clear-Field (Center $f[2]) 2; TypeAscii $code_ # Verify ALL THREE fields hold their intended value; on mismatch, retype the # offending field and re-check (max 2 retries) - a dropped key event on the # live device is a known flake (measured: port typed as "4780"), and the # final judgement must still be 'equals expected', never 'close enough'. $expect = @($host_, $port_, $code_) for ($retry=0; $retry -le 2; $retry++) { $x2 = Get-UiText $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) } } Write-Host " form now: $($vals -join ' | ') (try $retry)" 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 if ($f.Count -lt 3) { throw "form lost fields during retry" } Clear-Field (Center $f[$bad]) $bad TypeAscii $expect[$bad] continue } break } if ($vals[0] -ne $host_) { throw "form input FAIL: host field is [$($vals[0])], expected [$host_]" } if ($vals[1] -ne $port_) { throw "form input FAIL: port field is [$($vals[1])], expected [$port_]" } if ($vals[2] -ne $code_) { throw "form input FAIL: code field is [$($vals[2])], expected [$code_]" } $btn = $null foreach ($want in @($F_CONNECT, $F_RECONNECT)) { $btn = ButtonCenter $x2 $want; if ($btn) { break } } if (-not $btn) { throw "no primary connect/reconnect button found" } $t = Get-Date [void](AdbShell @("input","tap","$($btn[0])","$($btn[1])")) return $t } function Wait-Connecting([string]$blackhole, [double]$seconds) { # connecting state = remote panel up, bottom bar shows the CANCEL button $deadline = (Get-Date).AddSeconds($seconds) while ((Get-Date) -lt $deadline) { $x = Get-UiText if (ButtonCenter $x $F_CANCEL) { return $x } Start-Sleep -Milliseconds 250 } throw "never entered connecting state (no CANCEL button within ${seconds}s)" } Write-Host "=== 0. stale-agent guard (R-7) + IME disable (restore in finally) ===" if (-not $AgentExe) { $AgentExe = Join-Path $repo "target\release\agent.exe" } $ag = @(Get-Process agent -ErrorAction SilentlyContinue) if ($ag.Count -gt 0 -and (Test-Path $AgentExe) -and ((Get-Item $AgentExe).LastWriteTime -gt $ag[0].StartTime)) { Write-Host " running agent predates current build; restarting" Stop-Process -Name agent -Force -ErrorAction SilentlyContinue Start-Sleep -Milliseconds 800; $ag = @() } if ($ag.Count -eq 0) { if (-not (Test-Path $AgentExe)) { throw "missing $AgentExe (cargo build --release -p agent)" } Start-Process $AgentExe -ArgumentList "--port","$Port","--code",$Code -WindowStyle Hidden | Out-Null Start-Sleep -Seconds 1; Write-Host " agent started (v3)" } else { Write-Host " agent running and current" } $origIme = ((AdbShell @("settings","get","secure","default_input_method")) -join "").Trim() Write-Host " original IME: [$origIme] (N13 discipline: recorded, restored in finally)" if ($origIme -and $origIme -ne "null") { [void](AdbShell @("ime","disable",$origIme)) } # keep the screen awake for the whole run (measured 2026-10-31: mid-run screen # timeout turned every later tap/keyevent into a no-op against the AOD - the # 'cancel did not return to form' FAIL was the sleep, not the app; a second # probe reproduced it with dump texts showing clock+battery instead of the app) [void](AdbShell @("svc","power","stayon","true")) try { # ================================================================= Write-Host "=== 1. V-A2a: wrong code -> AUTH class text + detail line ===" $x = Restart-AppForm [void](FillAndConnect $x $Host_ "$Port" "wrongcode3a") $deadline = (Get-Date).AddSeconds(8); $seenAuth = $false; $seenDetail = $false; $shown = "" while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 400 $shown = ((VisibleTexts (Get-UiText)) -join " || ") if ($shown.Contains($F_AUTH)) { $seenAuth = $true } if ($shown.Contains("detail:")) { $seenDetail = $true } if ($seenAuth) { break } } & powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-a2-auth.png" | Out-Null Write-Host " screen: $shown" if (-not $seenAuth) { throw "V-A2a FAIL: no AUTH text ('$F_AUTH'); got [$shown]" } if (-not $seenDetail) { throw "V-A2a FAIL: AUTH shown but no 'detail:' original-error line; got [$shown]" } Write-Host " PASS: wrong code -> '$F_AUTH' + detail line" # ================================================================= Write-Host "=== 2. V-A1a: unresolvable host -> RESOLVE fast (NOT redialed) ===" $x = Restart-AppForm $t = FillAndConnect $x "999.999.999.999" "$Port" $Code $deadline = $t.AddSeconds(3); $seen = $false; $shown = ""; $elapsed = -1 while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 200 $shown = ((VisibleTexts (Get-UiText)) -join " || ") if ($shown.Contains($F_RESOLVE)) { $seen = $true; $elapsed = ((Get-Date) - $t).TotalSeconds; break } } & powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-a1-resolve.png" | Out-Null Write-Host " screen: $shown (after ${elapsed}s)" if (-not $seen) { throw "V-A1a FAIL: no RESOLVE text ('$F_RESOLVE') within 3s; got [$shown]" } Write-Host " PASS: unresolvable host -> '$F_RESOLVE' in ${elapsed}s (<3s proves no redial)" # ================================================================= Write-Host "=== 3. V-A1b: blackhole IP -> CANCEL tap -> 'cancelled' form ===" # Intent path (2026-10-31 lesson): with the form path, taps during connecting # proved unreliable in THIS app - the EditText still holds focus when the # panel swaps (the form's own click chain leaves focus where it was), and the # first tap only defocuses: manual form-path probe lost the tap 4 rounds in a # row, while the intent path (no EditText ever focused) took ONE tap and # returned '连接已取消' instantly. Intent auto-connect reaches the very same # connecting state (same startConnect code), so it isolates the product # behaviour from keyboard/focus noise. [void](AdbShell @("input","keyevent","224")) [void](AdbShell @("wm","dismiss-keyguard")) [void](AdbShell @("am","force-stop","com.verify.remote")) Start-Sleep -Milliseconds 600 [void](AdbShell @("am","start","-n","com.verify.remote/.MainActivity", "--es","host","192.0.2.1","--ei","port","$Port","--es","code",$Code)) $xc = Wait-Connecting "192.0.2.1" 6 $cancel = ButtonCenter $xc $F_CANCEL if (-not $cancel) { throw "V-A1b FAIL: no CANCEL button in intent connecting state" } $tc = Get-Date [void](AdbShell @("input","tap","$($cancel[0])","$($cancel[1])")) $deadline = $tc.AddSeconds(3); $back = $false; $shown = "" while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 250 $x2 = Get-UiText $shown = ((VisibleTexts $x2) -join " || ") # judgement = the CANCEL-triggered text specifically (F_CANCELED), not # merely 'back at the form' - the 8s budget would also return a form # (with the timeout text) and that must not masquerade as a cancel PASS. if ((FormFields $x2).Count -ge 3 -and $shown.Contains($F_CANCELED)) { $back = $true; break } } & powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-a1-cancel.png" | Out-Null Write-Host " after cancel: $shown" if (-not $back) { throw "V-A1b FAIL: cancel tap did not surface '$F_CANCELED' within 3s; got [$shown]" } $ms = [int]((Get-Date).Subtract($tc).TotalMilliseconds) Write-Host " PASS: connecting -> CANCEL -> '$F_CANCELED' form in ${ms}ms" # ================================================================= Write-Host "=== 4. V-A4: BACK while connecting -> cancel, activity survives ===" # same intent-path rationale as step 3 (BACK also needs the app window to # own input focus; a focused EditText would eat the first gesture instead) [void](AdbShell @("input","keyevent","224")) [void](AdbShell @("wm","dismiss-keyguard")) [void](AdbShell @("am","force-stop","com.verify.remote")) Start-Sleep -Milliseconds 600 [void](AdbShell @("am","start","-n","com.verify.remote/.MainActivity", "--es","host","192.0.2.1","--ei","port","$Port","--es","code",$Code)) [void](Wait-Connecting "192.0.2.1" 6) [void](AdbShell @("input","keyevent","4")) # BACK $deadline = (Get-Date).AddSeconds(3); $atForm = $false; $shown = "" while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 250 $x2 = Get-UiText $shown = ((VisibleTexts $x2) -join " || ") if ((FormFields $x2).Count -ge 3 -and $shown.Contains($F_CANCELED)) { $atForm = $true; break } } $focusLine = ((AdbShell @("dumpsys","window")) -join "`n") $alive = $focusLine -match "com\.verify\.remote" & powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-a4-back.png" | Out-Null Write-Host " after BACK: activityAlive=$alive atForm=$atForm" Write-Host " screen: $shown" if (-not $alive) { throw "V-A4 FAIL: BACK during connect finished the activity (old super.onKeyDown regression)" } if (-not $atForm) { throw "V-A4 FAIL: BACK during connect did not surface '$F_CANCELED' on the form" } Write-Host " PASS: BACK during connect -> cancel + form, activity kept" # ================================================================= Write-Host "=== 5. V-A5: live session HUD: fps= present, no coordinate noise ===" $x = Restart-AppForm [void](FillAndConnect $x $Host_ "$Port" $Code) $deadline = (Get-Date).AddSeconds(12); $live = $false; $hud = "" while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500 $m = [regex]::Match((Get-UiText),'text="([^"]*RTT=[^"]*)"') if ($m.Success) { $hud = [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value); $live = $true; break } } $hasFps = $hud.Contains("fps=") $noNoise = -not ($hud.Contains("norm=") -or $hud.Contains("view=")) & powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-a5-hud.png" | Out-Null Write-Host " HUD: $hud" if (-not $live) { throw "V-A5 FAIL: session never reached live state" } if (-not $hasFps) { throw "V-A5 FAIL: HUD missing fps= ; got [$hud]" } if (-not $noNoise) { throw "V-A5 FAIL: HUD carries coordinate noise (norm=/view=)" } Write-Host " PASS: live HUD shows fps, no coordinate noise" Write-Host "=== 5b. V-A3 re-check: kill agent -> 'lost' form within ~16s ===" Stop-Process -Name agent -Force -ErrorAction SilentlyContinue $deadline = (Get-Date).AddSeconds(16); $lost = $false; $shown = "" while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500 $x3 = Get-UiText $shown = ((VisibleTexts $x3) -join " || ") if ($shown.Contains($F_KILLMSG) -and (FormFields $x3).Count -ge 3) { $lost = $true; break } } & powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-a3-lost.png" | Out-Null Write-Host " after kill: $shown" if (-not $lost) { throw "V-A3 FAIL: kill did not surface '$F_KILLMSG' form within 16s" } Write-Host " PASS: agent killed -> lost-session surfaces on form (idle-timeout bound)" Write-Host "=== 6. reconnect to a fresh agent (self-heal path) ===" Start-Process $AgentExe -ArgumentList "--port","$Port","--code",$Code -WindowStyle Hidden | Out-Null Start-Sleep -Seconds 2 $x = Get-UiText $btn = ButtonCenter $x $F_RECONNECT if (-not $btn) { $x = Restart-AppForm; $btn = $null } if ($btn) { [void](AdbShell @("input","tap","$($btn[0])","$($btn[1])")) $deadline = (Get-Date).AddSeconds(14); $live2 = $false while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500 if ((Get-UiText) -match "RTT=") { $live2 = $true; break } } if (-not $live2) { throw "step 6 FAIL: reconnect button did not reach a live session" } Write-Host " PASS: one-tap reconnect after agent restart works" [void](AdbShell @("input","keyevent","4")) # disconnect, free the slot Start-Sleep -Milliseconds 600 } else { Write-Host " (no live reconnect path exercised this run - covered by manual run)" } } finally { [void](AdbShell @("svc","power","stayon","false")) # undo the keep-awake above if ($origIme -and $origIme -ne "null") { [void](AdbShell @("ime","enable",$origIme)) [void](AdbShell @("ime","set",$origIme)) Write-Host " IME restored: $(((AdbShell @('settings','get','secure','default_input_method')) -join '').Trim())" } } Write-Host "=== CONNECT LIFECYCLE PROOF COMPLETE ==="