diff --git a/tools/capture_evidence.ps1 b/tools/capture_evidence.ps1 new file mode 100644 index 0000000..97bebd5 --- /dev/null +++ b/tools/capture_evidence.ps1 @@ -0,0 +1,143 @@ +# 2b evidence capture: byte-safe device screenshots of each acceptance state. +# ASCII-only source. Screenshots go through tools\shot.ps1 because PS 5.1 `>` +# writes UTF-16 and corrupts PNG bytes (see that file's header). +# Text/state assertions use uiautomator (lessons R-6: vivo logcat is throttled). + +$ErrorActionPreference = "Stop" +$repo = (Resolve-Path "$PSScriptRoot\..").Path +$ev = "D:\working\projects\yuankong\evidence\2b" +$adb = "D:\working\android\sdk\platform-tools\adb.exe" +$phoneIp = "192.168.1.180" + +function Adb-Shell { + param([Parameter(Mandatory)][string[]]$Cmd) + $old = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + $out = & $adb shell @Cmd 2>$null + return @{ out = ($out -join "`n"); code = $LASTEXITCODE } + } + finally { $ErrorActionPreference = $old } +} + +function Get-Ui { + [void](Adb-Shell @("uiautomator", "dump", "/sdcard/ui.xml")) + $old = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { & $adb pull /sdcard/ui.xml "$env:TEMP\cap-ui.xml" 2>$null | Out-Null } + finally { $ErrorActionPreference = $old } + if (-not (Test-Path "$env:TEMP\cap-ui.xml")) { return "" } + return [System.IO.File]::ReadAllText("$env:TEMP\cap-ui.xml", [System.Text.Encoding]::UTF8) +} + +function Wait-Live([int]$Seconds) { + $deadline = (Get-Date).AddSeconds($Seconds) + while ((Get-Date) -lt $deadline) { + Start-Sleep -Milliseconds 900 + $x = Get-Ui + if ($x -match "RTT=") { return $x } + } + return "" +} + +function Shot([string]$name) { + $out = Join-Path $ev $name + & powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out $out +} + +New-Item -ItemType Directory -Force -Path $ev | Out-Null +# drop the earlier broken captures (PS 5.1 '>' wrote them as UTF-16) +Get-ChildItem $ev -Filter "*.png" | ForEach-Object { + $b = [System.IO.File]::ReadAllBytes($_.FullName) + if ($b.Length -lt 4 -or ($b[0] -ne 0x89)) { Remove-Item $_.FullName -Force } +} + +Write-Host "=== ensure fresh v3 agent ===" +$agentExe = "$repo\target\release\agent.exe" +$ag = Get-Process agent -ErrorAction SilentlyContinue +if ($ag -and (Get-Item $agentExe).LastWriteTime -gt $ag.StartTime) { + Stop-Process -Name agent -Force -ErrorAction SilentlyContinue + Start-Sleep -Milliseconds 800; $ag = $null +} +if (-not $ag) { + Start-Process $agentExe -ArgumentList "--port","47800","--code","t" -WindowStyle Hidden + Start-Sleep -Seconds 1 +} + +Write-Host "=== 1. connect form (V4 entry state) ===" +[void](Adb-Shell @("am","force-stop","com.verify.remote")) +[void](Adb-Shell @("am","start","-n","com.verify.remote/.MainActivity")) +Start-Sleep -Seconds 3 +$ui = Get-Ui +if ($ui -notmatch "android\.widget\.EditText") { throw "form fields missing" } +Shot "4b-form.png" + +Write-Host "=== 2. failure path (V5): unreachable host via intent ===" +[void](Adb-Shell @("am","force-stop","com.verify.remote")) +[void](Adb-Shell @("am","start","-n","com.verify.remote/.MainActivity", + "--es","host","192.168.1.199","--ei","port","47800","--es","code","t")) +# Measured 2026-10-30: an unreachable host returns to the form with a concrete +# reason after ~32s (quinn's default connect timeout). Budget 50s, not 30s - +# the earlier 30s budget made this step look like a failure when it was only +# still inside the timeout. UX follow-up (logged in the result record): during +# those 30s the panel says only "connecting" with no cancel affordance. +$deadline = (Get-Date).AddSeconds(50) +$err = "" +while ((Get-Date) -lt $deadline) { + Start-Sleep -Milliseconds 1000 + $ui = Get-Ui + if ($ui -match "android\.widget\.EditText") { + $shown = [regex]::Matches($ui, 'text="([^"]+)"') | ForEach-Object { $_.Groups[1].Value } + $err = $shown -join " | " + break + } +} +if ($err) { Shot "4b-v5-error.png" } else { throw "V5: never returned to form with an error" } +Write-Host " error state visible: yes" + +Write-Host "=== 3. live session (V4 via intent) ===" +[void](Adb-Shell @("am","force-stop","com.verify.remote")) +[void](Adb-Shell @("am","start","-n","com.verify.remote/.MainActivity", + "--es","host",$phoneIp,"--ei","port","47800","--es","code","t")) +$ui = Wait-Live 25 +if (-not $ui) { throw "session never became live" } +Shot "4b-connected-stream.png" +Write-Host " $([regex]::Match($ui,'text="([^"]*RTT=[^"]*)"').Groups[1].Value)" + +Write-Host "=== 4. keyboard shown (real IME bound to our InputConnection) ===" +$btns = [regex]::Matches($ui, ']*class="android\.widget\.Button"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') +if ($btns.Count -lt 1) { throw "no keyboard button" } +$b0 = $btns[0] +$kx = [int](([int]$b0.Groups[1].Value + [int]$b0.Groups[3].Value) / 2) +$ky = [int](([int]$b0.Groups[2].Value + [int]$b0.Groups[4].Value) / 2) +[void](Adb-Shell @("input","tap","$kx","$ky")) +Start-Sleep -Seconds 3 +$ime = (Adb-Shell @("dumpsys","input_method")).out +if ($ime -notmatch "mInputShown=true") { throw "IME not shown" } +if ($ime -notmatch "mHaveConnection=true") { throw "no InputConnection bound" } +Shot "4b-ime-shown.png" + +Write-Host "=== 5. IME commit reaches the pipeline (diagnostic shows commit) ===" +[void](Adb-Shell @("input","text","hello")) +Start-Sleep -Milliseconds 1200 +[void](Adb-Shell @("input","keyevent","62")) # SPACE: accept any pending candidate +Start-Sleep -Seconds 2 +$ui = Get-Ui +$diag = [regex]::Match($ui, 'text="([^"]*RTT=[^"]*)"').Groups[1].Value +Write-Host " phone status: $diag" +Shot "4b-ime-committed.png" + +Write-Host "=== 6. disconnect (back key) ===" +[void](Adb-Shell @("input","keyevent","4")) # dismiss keyboard +Start-Sleep -Milliseconds 800 +[void](Adb-Shell @("input","keyevent","4")) # disconnect +Start-Sleep -Seconds 2 +$ui = Get-Ui +if ($ui -match "RTT=") { throw "still connected after two BACK presses" } +Shot "4b-disconnected.png" +Write-Host "=== CAPTURE COMPLETE ===" +Get-ChildItem $ev -Filter "4b-*.png" | ForEach-Object { + $b = [System.IO.File]::ReadAllBytes($_.FullName) + $sig = ($b[0..3] | ForEach-Object { $_.ToString("X2") }) -join " " + "{0,-28} {1,9} {2}" -f $_.Name, $b.Length, $sig +} diff --git a/tools/ime_commit_proof.ps1 b/tools/ime_commit_proof.ps1 new file mode 100644 index 0000000..37352a5 --- /dev/null +++ b/tools/ime_commit_proof.ps1 @@ -0,0 +1,182 @@ +# 2b V6/V7 on-device IME path: does a real IME's commitText reach the remote +# desktop? ASCII-only source (see personal lessons on PS 5.1 + BOM-less .ps1). +# +# Difference from phone_chain_proof.ps1: that one drives Activity.onKeyDown with +# adb keyevents (IME hidden). Here the IME is SHOWN, so keys are consumed by the +# input method and reach us only through RemoteInputConnection.commitText / +# sendKeyEvent. That is the path Chinese candidates take, and it is the risk R1 +# item in the plan (vivo ROM + third-party IME behaviour was never observed). +# +# What this proves mechanically: IME is bound to our InputConnection and its +# committed text lands in a Windows window we own, via protocol v3. +# What it cannot prove: human candidate-list selection (needs a finger); see +# the result record - V6/V7 full judgement is listed as manual acceptance. +# +# Usage: +# powershell -ExecutionPolicy Bypass -File tools\ime_commit_proof.ps1 + +param( + [string]$PhoneIp = "192.168.1.180", + [int]$Port = 47800, + [string]$Code = "t", + [string]$Text = "hi" +) +$ErrorActionPreference = "Stop" +$repo = (Resolve-Path "$PSScriptRoot\..").Path +$adb = "D:\working\android\sdk\platform-tools\adb.exe" +$tmp = "$env:TEMP\remote-ime-proof" + +New-Item -ItemType Directory -Force -Path $tmp | Out-Null +$pidFile = "$tmp\target.pid" +$stopFile = "$tmp\stop.flag" +$resultFile = "$tmp\received.txt" +Remove-Item $pidFile,$stopFile,$resultFile -ErrorAction SilentlyContinue + +Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; +public static class FgApi2 { + [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); +} +"@ + +function Adb { + param([Parameter(Mandatory)][string[]]$AdbArgs) + $old = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + $out = & $adb @AdbArgs 2>$null + return @{ out = ($out -join "`n"); code = $LASTEXITCODE } + } + finally { $ErrorActionPreference = $old } +} +function Adb-Shell { param([Parameter(Mandatory)][string[]]$Cmd) return Adb -AdbArgs (@("shell") + $Cmd) } + +function Wait-File { + param([string]$Path, [int]$TimeoutMs = 20000) + $t = 0 + while (-not (Test-Path $Path) -and $t -lt ($TimeoutMs / 150)) { Start-Sleep -Milliseconds 150; $t++ } + if (-not (Test-Path $Path)) { throw "timeout waiting for $Path" } +} + +function Assert-Foreground([IntPtr]$hwnd, [string]$where) { + $deadline = (Get-Date).AddMilliseconds(2000) + while ((Get-Date) -lt $deadline) { + if ([FgApi2]::GetForegroundWindow() -eq $hwnd) { return } + Start-Sleep -Milliseconds 30 + } + throw "$where : foreground=$([FgApi2]::GetForegroundWindow()) want=$($hwnd.ToInt64()) - refusing to inject" +} + +function Get-PhoneUiText { + [void](Adb-Shell @("uiautomator", "dump", "/sdcard/ui.xml")) + $r = Adb -AdbArgs @("pull", "/sdcard/ui.xml", "$tmp\ui.xml") + if ($r.code -ne 0 -or -not (Test-Path "$tmp\ui.xml")) { return "" } + return [System.IO.File]::ReadAllText("$tmp\ui.xml", [System.Text.Encoding]::UTF8) +} + +function Read-Received { + if (-not (Test-Path $resultFile)) { return "" } + $b = [System.IO.File]::ReadAllBytes($resultFile) + $s = [System.Text.Encoding]::UTF8.GetString($b) + if ($s.Length -gt 0 -and $s[0] -eq [char]0xFEFF) { $s = $s.Substring(1) } + return $s +} + +Write-Host "=== 0. retire earlier targets (touch only existing stop flags) ===" +$flags = @(Get-ChildItem -Path $env:TEMP -Recurse -Filter "stop.flag" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*remote*proof*" }) +if ($flags.Count -gt 0) { + Write-Host " retiring $($flags.Count)" + foreach ($f in $flags) { (Get-Item $f.FullName).LastWriteTime = Get-Date } + Start-Sleep -Seconds 3 +} + +Write-Host "=== 1. fresh target window ===" +Start-Process powershell.exe -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass",` + "-File","$repo\tools\text_target.ps1","-PidFile",$pidFile,` + "-ResultFile",$resultFile,"-StopFile",$stopFile,"-GrabForeground" +Wait-File $pidFile +$parts = @((Get-Content $pidFile -Raw) -split "`n") +$tid = [int]$parts[0].Trim() +$hwnd = [IntPtr][long]$parts[1].Trim() +Assert-Foreground $hwnd "after launch" +Write-Host " target pid=$tid hwnd=$($hwnd.ToInt64())" + +Write-Host "=== 2. agent freshness + phone session ===" +$agentExe = "$repo\target\release\agent.exe" +$ag = Get-Process agent -ErrorAction SilentlyContinue +if ($ag -and (Get-Item $agentExe).LastWriteTime -gt $ag.StartTime) { + Stop-Process -Name agent -Force -ErrorAction SilentlyContinue + Start-Sleep -Milliseconds 800; $ag = $null +} +if (-not $ag) { + Start-Process $agentExe -ArgumentList "--port",$Port,"--code",$Code -WindowStyle Hidden + Start-Sleep -Seconds 1 +} +[void](Adb-Shell @("am", "force-stop", "com.verify.remote")) +[void](Adb-Shell @("am", "start", "-n", "com.verify.remote/.MainActivity", + "--es", "host", $PhoneIp, "--ei", "port", "$Port", "--es", "code", $Code)) +$deadline = (Get-Date).AddSeconds(25) +$xml = "" +while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 900; $xml = Get-PhoneUiText; if ($xml -match "RTT=") { break } } +if ($xml -notmatch "RTT=") { throw "phone did not connect" } +Write-Host " $([regex]::Match($xml,'text="([^"]*RTT=[^"]*)"').Groups[1].Value)" + +Write-Host "=== 3. show the soft keyboard (tap the app's keyboard button) ===" +# Button coordinates come from the live hierarchy. Our remote-state bar adds +# "keyboard" before "disconnect", so index 0 is the keyboard button - asserted +# rather than assumed by checking the count and printing what we tapped. +$btns = [regex]::Matches($xml, ']*class="android\.widget\.Button"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') +if ($btns.Count -lt 1) { throw "no buttons found in remote state" } +$b0 = $btns[0] +$kx = [int](([int]$b0.Groups[1].Value + [int]$b0.Groups[3].Value) / 2) +$ky = [int](([int]$b0.Groups[2].Value + [int]$b0.Groups[4].Value) / 2) +Write-Host " tapping first button ($kx,$ky)" +[void](Adb-Shell @("input", "tap", "$kx", "$ky")) +Start-Sleep -Seconds 3 +$ime = (Adb-Shell @("dumpsys", "input_method")).out +$shown = ($ime -match "mInputShown=true") +$bound = ($ime -match "mHaveConnection=true") +$curId = [regex]::Match($ime, "mCurId=(\S+)").Groups[1].Value +Write-Host " IME: shown=$shown boundToUs=$bound cur=$curId" +if (-not $shown) { throw "soft keyboard did not appear" } +if (-not $bound) { throw "no InputConnection bound - our onCheckIsTextEditor/onCreateInputConnection path failed" } + +Write-Host "=== 4. type through the IME, expect commitText -> v3 Text -> Windows ===" +$before = Read-Received +Assert-Foreground $hwnd "before IME text" +[void](Adb-Shell @("input", "text", $Text)) +Start-Sleep -Milliseconds 1500 +# Latin input in most Chinese IMEs commits without a candidate pick; if the IME +# instead left it composing, nudge with SPACE to force a commit. +$got = Read-Received +if ($got -notmatch [regex]::Escape($Text)) { + Write-Host " not committed yet (got [$got]); sending SPACE to accept candidate" + [void](Adb-Shell @("input", "keyevent", "62")) # KEYCODE_SPACE + Start-Sleep -Milliseconds 1500 + $got = Read-Received +} +Write-Host " target received: [$got] (expected to contain [$Text])" +$diag = [regex]::Match((Get-PhoneUiText), 'text="([^"]*RTT=[^"]*)"').Groups[1].Value +Write-Host " phone status: $diag" +if ($got -notmatch [regex]::Escape($Text)) { + throw "IME COMMIT FAIL: nothing from the IME reached the Windows target" +} +Write-Host " PASS: real IME -> RemoteInputConnection -> v3 Text -> Windows window" + +Write-Host "=== 5. back key must close keyboard first, not the session ===" +[void](Adb-Shell @("input", "keyevent", "4")) +Start-Sleep -Seconds 2 +$ime2 = (Adb-Shell @("dumpsys", "input_method")).out +$xml2 = Get-PhoneUiText +$stillLive = ($xml2 -match "RTT=") +$kbHidden = ($ime2 -match "mInputShown=false") +Write-Host " after BACK: session live=$stillLive keyboard hidden=$kbHidden" +if (-not ($kbHidden -and $stillLive)) { throw "BACK did not behave as keyboard-first-then-session" } +Write-Host " PASS: first BACK dismissed the keyboard, session kept" + +Write-Host "=== 6. cleanup ===" +New-Item -ItemType File -Force -Path $stopFile | Out-Null +Start-Sleep -Milliseconds 500 +Write-Host "=== IME COMMIT PROOF COMPLETE ===" diff --git a/tools/shot.ps1 b/tools/shot.ps1 new file mode 100644 index 0000000..bcab77f --- /dev/null +++ b/tools/shot.ps1 @@ -0,0 +1,23 @@ +# Byte-safe device screenshot helper. +# ASCII-only. PS 5.1 `>` redirection writes UTF-16, which corrupts PNG bytes +# (measured 2026-10-30: `adb exec-out screencap -p > x.png` produced a file +# starting with FF FE). Capture on-device and pull instead. +param( + [Parameter(Mandatory = $true)][string]$Out, + [string]$Device = "/sdcard/shot.png" +) +$ErrorActionPreference = "Stop" +$adb = "D:\working\android\sdk\platform-tools\adb.exe" +$old = $ErrorActionPreference +$ErrorActionPreference = "Continue" +try { + & $adb shell screencap -p $Device | Out-Null + if ($LASTEXITCODE -ne 0) { throw "screencap failed ($LASTEXITCODE)" } + & $adb pull $Device $Out | Out-Null + if ($LASTEXITCODE -ne 0) { throw "pull failed ($LASTEXITCODE)" } +} +finally { $ErrorActionPreference = $old } +$b = [System.IO.File]::ReadAllBytes($Out) +$sig = ($b[0..3] | ForEach-Object { $_.ToString("X2") }) -join " " +if ($sig -ne "89 50 4E 47") { throw "not a PNG (sig=$sig): $Out" } +Write-Host ("OK {0} ({1:N0} bytes)" -f (Split-Path $Out -Leaf), $b.Length)