# 2b V6 on-device CHINESE: drive the IME through pinyin + candidate selection. # ASCII-only source. Non-ASCII expectations are built from code points. # # Measured path differences (2026-10-30, vivo S6 + iFlytek IME): # - `adb input keyevent ` behaves like a HARDWARE keyboard: the IME # does not compose, the event lands in Activity.onKeyDown and our printable # unicodeChar branch sends it as text. So keyevents CANNOT exercise Chinese # candidate selection. # - `adb input text ` is delivered to the IME, which composes it; # SPACE (keyevent 62) then accepts the highlighted candidate, and the Hanzi # arrive at RemoteInputConnection.commitText. # This script therefore types pinyin with `input text` and commits with SPACE. # # Acceptance: the Windows target window receives the exact Han characters the # IME committed - Chinese input works end to end over protocol v3. # Not covered: swipe/handwriting input, browsing the candidate list. param( # PC (controlled side) LAN address. Default = the 2026-10-31 measured WLAN # address; it is DHCP-assigned, so pass -PhoneIp when the office network # moves (the stale .180 default silently cost a proof run on 2026-10-31). [string]$PhoneIp = "192.168.1.110", [int]$Port = 47800, [string]$Code = "t" ) $ErrorActionPreference = "Stop" $repo = (Resolve-Path "$PSScriptRoot\..").Path $adb = "D:\working\android\sdk\platform-tools\adb.exe" $tmp = "$env:TEMP\remote-cjk2" 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; using System.Text; public static class Fg5 { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid); [DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n); public static string Desc() { IntPtr h = GetForegroundWindow(); uint pid; GetWindowThreadProcessId(h, out pid); var sb = new StringBuilder(256); GetWindowText(h, sb, 256); return h.ToInt64() + " [" + sb.ToString() + "] pid=" + pid; } } "@ 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 Assert-Foreground([IntPtr]$hwnd, [string]$where) { $deadline = (Get-Date).AddMilliseconds(2500) while ((Get-Date) -lt $deadline) { if ([Fg5]::GetForegroundWindow() -eq $hwnd) { return } Start-Sleep -Milliseconds 50 } throw "$where : foreground is $([Fg5]::Desc()), want target $($hwnd.ToInt64()) - refusing to inject" } function Get-Ui { [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 (-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 } function Show-Cps([string]$s) { ($s.ToCharArray() | ForEach-Object { "U+{0:X4}" -f [int]$_ }) -join " " } Write-Host "=== 0. retire ALL earlier targets, verify none holds foreground ===" $flags = @(Get-ChildItem -Path $env:TEMP -Recurse -Filter "stop.flag" -ErrorAction SilentlyContinue | Where-Object { $_.FullName -like "*remote*cjk*" -or $_.FullName -like "*remote*proof*" }) foreach ($f in $flags) { (Get-Item $f.FullName).LastWriteTime = Get-Date } if ($flags.Count -gt 0) { Start-Sleep -Seconds 3 } $hosts = @(Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" | Where-Object { $_.CommandLine -match 'text_target\.ps1' -and $_.ProcessId -ne $PID }) foreach ($hx in $hosts) { Stop-Process -Id $hx.ProcessId -Force -ErrorAction SilentlyContinue } if ($hosts.Count -gt 0) { Start-Sleep -Seconds 1 } Write-Host " retired $($flags.Count) flag(s), killed $($hosts.Count) host(s); fg now: $([Fg5]::Desc())" Write-Host "=== 1. agent freshness ===" $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 "=== 2. fresh target window (empty field) ===" 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" $base0 = Read-Received Write-Host " target pid=$tid hwnd=$($hwnd.ToInt64()) baseline=[$(Show-Cps $base0)]" Write-Host "=== 3. phone session ===" [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; $ui = Get-Ui; if ($ui -match "RTT=") { break } } if ($ui -notmatch "RTT=") { throw "phone did not connect" } Write-Host "=== 4. show IME (tap keyboard button) ===" $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: $([regex]::Match($ime,'mCurId=(\S+)').Groups[1].Value)" Write-Host "=== 4b/5. pinyin -> IME candidate -> commitText (mode-tolerant) ===" # Both IMEs on this device may remember an ENGLISH mode, in which case letters # pass through without composing - which looks identical to "our pipeline lost # the Hanzi" unless you check. The IME's key views are NOT visible to # uiautomator (its dump contains only our app's nodes), so the ab/zh toggle is # reached by a calibrated tap: iFlytek's key sits at display (73,1048) in a # 1080x2400 capture => device (146,2096) at 2x. Verified 2026-10-30. # The toggle is STATEFUL, so this is written as an idempotent attempt loop: # try, and only if we see letters-without-Hanzi do we flip the mode and retry. # A stray tap here changes a keyboard setting, nothing destructive. $modeToggle = @(146, 2096) $expect = -join @([char]0x4F60, [char]0x597D) # NI HAO $attempt = 0 $new = "" while ($attempt -lt 2) { $attempt++ $base = Read-Received Assert-Foreground $hwnd "attempt ${attempt}: before pinyin" [void](Adb-Shell @("input","text","nihao")) Start-Sleep -Milliseconds 1500 $mid = Read-Received Write-Host " attempt ${attempt} after pinyin (composing): [$(Show-Cps $mid)]" Assert-Foreground $hwnd "attempt ${attempt}: before SPACE" [void](Adb-Shell @("input","keyevent","62")) Start-Sleep -Seconds 2 $got = Read-Received $new = if ($got.Length -ge $base.Length) { $got.Substring($base.Length) } else { "" } Write-Host " attempt ${attempt} newly received: [$(Show-Cps $new)]" if ($new -match [regex]::Escape($expect)) { break } if ($attempt -eq 1) { Write-Host " no Hanzi yet -> flipping IME language mode and retrying once" [void](Adb-Shell @("input","tap","$($modeToggle[0])","$($modeToggle[1])")) Start-Sleep -Seconds 2 } } $diag = [regex]::Match((Get-Ui), 'text="([^"]*RTT=[^"]*)"').Groups[1].Value Write-Host " phone status: $diag" Write-Host " expected to contain: $(Show-Cps $expect)" if ($new -notmatch [regex]::Escape($expect)) { Write-Host " GROUND TRUTH: [$(Show-Cps $new)]" Write-Host " U+006E.. letters only => IME stayed in English mode (re-calibrate the toggle tap)" throw "V6 CHINESE FAIL: IME did not deliver Hanzi through commitText" } Write-Host " V6 PASS: pinyin -> IME candidate -> commitText -> v3 Text -> Windows Hanzi" Write-Host "=== 6. cleanup ===" [void](Adb-Shell @("input","keyevent","4")) New-Item -ItemType File -Force -Path $stopFile | Out-Null Start-Sleep -Milliseconds 500 Write-Host "=== CHINESE IME PROOF COMPLETE ==="