diff --git a/tools/emoji_manual_proof.ps1 b/tools/emoji_manual_proof.ps1 new file mode 100644 index 0000000..e1855ca --- /dev/null +++ b/tools/emoji_manual_proof.ps1 @@ -0,0 +1,191 @@ +# 2b V7 (device side): manual emoji acceptance helper. +# ASCII-only source (PS 5.1 + BOM-less = GBK corruption; see personal lessons). +# +# WHY MANUAL: the IME's own emoji panel is invisible to uiautomator (dump shows +# only our app's nodes), so adb cannot drive emoji selection. The human taps the +# emoji; this script only prepares the receiving window and reads back what +# actually arrived - the judgement is still made against real received bytes. +# +# What this proves: the phone IME commits a supplementary-plane character +# (surrogate pair) as ONE commitText, and protocol v3 delivers both code units +# adjacently so Windows synthesises a single character. +# The Windows half is already proven mechanically (V7' in text_injection_proof). +# +# Usage: +# powershell -ExecutionPolicy Bypass -File tools\emoji_manual_proof.ps1 +# ... then, when prompted, tap an emoji on the phone's keyboard. + +param( + [string]$PhoneIp = "192.168.1.180", + [int]$Port = 47800, + [string]$Code = "t", + [int]$WaitSeconds = 120 +) +$ErrorActionPreference = "Stop" +$repo = (Resolve-Path "$PSScriptRoot\..").Path +$adb = "D:\working\android\sdk\platform-tools\adb.exe" +$tmp = "$env:TEMP\remote-emoji-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 FgEmoji { + [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); +} +"@ + +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 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 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 +} + +function Show-Cps([string]$s) { + ($s.ToCharArray() | ForEach-Object { "U+{0:X4}" -f [int]$_ }) -join " " +} + +Write-Host "=== 1. retire earlier targets, ensure fresh agent ===" +$flags = @(Get-ChildItem -Path $env:TEMP -Recurse -Filter "stop.flag" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*remote*proof*" }) +foreach ($f in $flags) { (Get-Item $f.FullName).LastWriteTime = Get-Date } +if ($flags.Count -gt 0) { Start-Sleep -Seconds 3 } + +$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 + Write-Host " agent started" +} + +Write-Host "=== 2. target window (receives the injected text) ===" +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() +Start-Sleep -Milliseconds 800 +$fg = [FgEmoji]::GetForegroundWindow() +Write-Host " target pid=$tid hwnd=$($hwnd.ToInt64()) foreground=$($fg -eq $hwnd)" + +Write-Host "=== 3. phone session + show keyboard ===" +[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) +$ui = "" +while ((Get-Date) -lt $deadline) { + Start-Sleep -Milliseconds 900 + [void](Adb-Shell @("uiautomator","dump","/sdcard/ui.xml")) + $old = $ErrorActionPreference; $ErrorActionPreference = "Continue" + try { & $adb pull /sdcard/ui.xml "$tmp\ui.xml" 2>$null | Out-Null } finally { $ErrorActionPreference = $old } + if (Test-Path "$tmp\ui.xml") { $ui = [System.IO.File]::ReadAllText("$tmp\ui.xml", [System.Text.Encoding]::UTF8) } + if ($ui -match "RTT=") { break } +} +if ($ui -notmatch "RTT=") { throw "phone did not connect" } + +$btns = [regex]::Matches($ui, ']*class="android\.widget\.Button"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') +if ($btns.Count -lt 1) { throw "keyboard button not found" } +$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 "IME not bound to our InputConnection" } +Write-Host " IME shown and bound: $([regex]::Match($ime,'mCurId=(\S+)').Groups[1].Value)" + +$baseline = Read-Received +Write-Host "" +Write-Host " ------------------------------------------------------------" +Write-Host " NOW ON THE PHONE: tap the emoji key, then tap ONE emoji." +Write-Host " (Keyboard is up; the Windows target window is receiving.)" +Write-Host " Waiting up to $WaitSeconds s for a supplementary-plane character..." +Write-Host " ------------------------------------------------------------" +Write-Host "" + +$deadline = (Get-Date).AddSeconds($WaitSeconds) +$got = "" +$found = $false +while ((Get-Date) -lt $deadline) { + Start-Sleep -Milliseconds 500 + $got = Read-Received + if ($got.Length -gt $baseline.Length) { + $new = $got.Substring($baseline.Length) + # any code unit in the surrogate range means an emoji (or other + # supplementary-plane char) reached us + foreach ($ch in $new.ToCharArray()) { + $cp = [int]$ch + if ($cp -ge 0xD800 -and $cp -le 0xDFFF) { $found = $true; break } + } + if ($found) { break } + } +} + +$final = Read-Received +$newFinal = if ($final.Length -ge $baseline.Length) { $final.Substring($baseline.Length) } else { "" } +Write-Host "=== 4. result ===" +Write-Host " baseline : [$(Show-Cps $baseline)]" +Write-Host " final : [$(Show-Cps $final)]" +Write-Host " new chars: [$(Show-Cps $newFinal)]" +Write-Host " new units: $($newFinal.Length)" + +if ($found) { + # verify the pair is adjacent and forms a real code point + $units = $newFinal.ToCharArray() | ForEach-Object { [int]$_ } + $paired = $false + for ($i = 0; $i -lt $units.Count - 1; $i++) { + if ($units[$i] -ge 0xD800 -and $units[$i] -le 0xDBFF -and + $units[$i+1] -ge 0xDC00 -and $units[$i+1] -le 0xDFFF) { + $cp = 0x10000 + (($units[$i] - 0xD800) * 0x400) + ($units[$i+1] - 0xDC00) + Write-Host (" V7 PASS: adjacent surrogate pair -> U+{0:X4} ({1} code units committed as one)" -f $cp, $newFinal.Length) + $paired = $true + break + } + } + if (-not $paired) { throw "V7 FAIL: surrogate units present but not an adjacent valid pair" } + if ($newFinal.Contains([char]0xFFFD)) { throw "V7 FAIL: U+FFFD present (pair was split)" } +} +else { + Write-Host " V7 INCONCLUSIVE: no supplementary-plane character arrived within $WaitSeconds s" + Write-Host " (If you tapped an emoji and nothing arrived, that IS the finding - record it.)" +} + +Write-Host "=== 5. cleanup ===" +[void](Adb-Shell @("input","keyevent","4")) +New-Item -ItemType File -Force -Path $stopFile | Out-Null +Start-Sleep -Milliseconds 500 +Write-Host "=== done ===" diff --git a/tools/phone_chain_proof.ps1 b/tools/phone_chain_proof.ps1 index 4fd3914..89d200f 100644 --- a/tools/phone_chain_proof.ps1 +++ b/tools/phone_chain_proof.ps1 @@ -222,9 +222,18 @@ try { [void](Adb-Shell @("input", "tap", "$($ipC[0])", "$($ipC[1])")) Start-Sleep -Milliseconds 400 for ($i = 0; $i -lt 24; $i++) { [void](Adb-Shell @("input", "keyevent", "67")) } - foreach ($k in @(8,16,9,56,8,13,15,56,8,56,8,15,7)) { # 192.168.1.180 - [void](Adb-Shell @("input", "keyevent", "$k")) + # Type the SAME host this run was given. A hardcoded literal broke this + # proof on 2026-10-31 when the dev box's LAN address changed + # (192.168.1.180 -> 192.168.1.110): the form dialed the stale address, + # timed out, and it looked like a product failure when the harness was + # simply out of date. Digits are KEYCODE_0..9 = 7..16, '.' = 56. + $ipKeys = @() + foreach ($ch in $PhoneIp.ToCharArray()) { + if ($ch -eq '.') { $ipKeys += 56 } + elseif ($ch -ge '0' -and $ch -le '9') { $ipKeys += (7 + [int][string]$ch) } + else { throw "unsupported character in PhoneIp: $ch" } } + foreach ($k in $ipKeys) { [void](Adb-Shell @("input", "keyevent", "$k")) } [void](Adb-Shell @("input", "tap", "$($codeC[0])", "$($codeC[1])")) Start-Sleep -Milliseconds 400 for ($i = 0; $i -lt 6; $i++) { [void](Adb-Shell @("input", "keyevent", "67")) } @@ -244,7 +253,7 @@ try { # so "field text == typed value" is the assertion, not "field text non-empty". Write-Host " form now: $($vals -join ' | ')" if ($vals.Count -lt 3) { throw "expected 3 fields after typing, got $($vals.Count)" } - if ($vals[0] -ne "192.168.1.180") { throw "V4 FAIL: IP field is [$($vals[0])], typing did not land" } + if ($vals[0] -ne $PhoneIp) { throw "V4 FAIL: IP field is [$($vals[0])], expected [$PhoneIp]" } $btn = [regex]::Match($xml3, ']*class="android\.widget\.Button"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') if (-not $btn.Success) { throw "connect button not found" } $bc = Center $btn