# 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. DHCP-assigned and DRIFTED twice # (.180 -> .110 -> .112); a stale default silently dials a dead host # (measured 2026-11-01: this run failed "phone did not connect" because the # caller passed -Host_, which does NOT bind to this parameter, so the # default was used). Always pass -PhoneIp explicitly and check the real IP. [string]$PhoneIp = "192.168.1.112", [int]$Port = 47800, [string]$Code = "t" ) $ErrorActionPreference = "Stop" $repo = (Resolve-Path "$PSScriptRoot\..").Path $adb = "D:\working\android\sdk\platform-tools\adb.exe" # UNIQUE per-run dir: with a fixed path, the startup Remove-Item deletes the # stop.flag that leftover targets from a previous failed run are watching, so # they can never retire and they pollute the shared received.txt # (same class of bug measured 2026-11-01 in phone_chain_proof). $tmp = "$env:TEMP\remote-cjk2-$PID" 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 " " } # ---- 4a landscape adaptations ---- # In landscape the HUD TextView hides WITH the auto-hiding control bar, so a # plain "RTT=" read returns nothing while the session is alive, and the # keyboard button is not in the tree at all. Both reads must wake through the # letterbox black bar first (that tap is consumed by the app; it never reaches # the remote). NB: this script's Adb-Shell returns @{out;code}, not a string. function Phone-DisplaySize { $d = (Adb-Shell @("dumpsys", "window", "displays")).out $idx = $d.IndexOf("Display: mDisplayId=0") if ($idx -lt 0) { return @{ w = 1080; h = 2400 } } $m = [regex]::Match($d.Substring($idx), 'cur=(\d+)x(\d+)') if (-not $m.Success) { return @{ w = 1080; h = 2400 } } return @{ w = [int]$m.Groups[1].Value; h = [int]$m.Groups[2].Value } } function Wake-Controls-If-Hidden([string]$xml) { $hidden = ($xml -match 'class="android\.view\.View"') -and ($xml -notmatch 'class="android\.widget\.Button"') if (-not $hidden) { return $false } $pd = Phone-DisplaySize [void](Adb-Shell @("input", "tap", "100", "$([int]($pd.h / 2))")) Start-Sleep -Milliseconds 400 return $true } # Read the tree until it shows a live session, waking the controls as needed. function Get-Ui-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 } if (Wake-Controls-If-Hidden $x) { $x = Get-Ui if ($x -match "RTT=") { return $x } } } return (Get-Ui) } # Find a Button by label prefix in a tree that may have auto-hidden controls. function Find-ButtonByPrefix([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)) { $b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') if ($b.Success) { return @( [int]((([int]$b.Groups[1].Value) + ([int]$b.Groups[3].Value)) / 2), [int]((([int]$b.Groups[2].Value) + ([int]$b.Groups[4].Value)) / 2), $t) } } } return $null } function Find-ButtonByPrefix-Live([string]$prefix) { for ($t=0; $t -lt 4; $t++) { $x = Get-Ui $b = Find-ButtonByPrefix $x $prefix if ($b) { return $b } if (-not (Wake-Controls-If-Hidden $x)) { Start-Sleep -Milliseconds 400 } } return $null } # Force PORTRAIT for this proof. Why: the IME's ab/zh language key is NOT visible # to uiautomator (its key views are separate windows), so the toggle is reached by # a CALIBRATED tap that is portrait-specific (146,2096 on 1080x2400). In landscape # that y is off-screen, so the retry path could not work. The text pipeline itself # is orientation-independent (landscape 'abc' is already covered by # phone_chain_proof), so running V6 in portrait costs nothing in coverage. function Set-Orientation-Portrait { $F_ORIENT = -join ([char]0x65B9,[char]0x5411,[char]0x3A) # "fangxiang:" $F_PORTRAIT = $F_ORIENT + -join ([char]0x7AD6,[char]0x5C4F) # "...:portrait" for ($t=0; $t -lt 8; $t++) { $b = Find-ButtonByPrefix-Live $F_ORIENT if (-not $b) { throw "orientation button not reachable" } if ($b[2] -eq $F_PORTRAIT) { Write-Host " orientation: already portrait"; return } [void](Adb-Shell @("input","tap","$($b[0])","$($b[1])")) Start-Sleep -Milliseconds 1300 } throw "could not reach portrait orientation after 8 taps" } 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)) $ui = Get-Ui-Live 25 if ($ui -notmatch "RTT=") { throw "phone did not connect" } Write-Host " live: $([regex]::Match($ui,'text="([^"]*RTT=[^"]*)"').Groups[1].Value)" # 4a: pin PORTRAIT before driving the IME (calibrated toggle tap is portrait-only) Set-Orientation-Portrait Write-Host "=== 4. show IME (tap keyboard button) ===" # Select the keyboard button BY ITS LABEL, not by position. # 3b added a sensitivity row (- / +) above the button bar, so "the first Button" # is now the minus sign - the old positional assumption silently tapped the wrong # control and the proof reported "IME not shown" (a harness defect, not a product # regression; measured 2026-10-31). Label matching is order-independent. $F_KEYBOARD = -join ([char]0x952E,[char]0x76D8) # "keyboard" $kb = Find-ButtonByPrefix-Live $F_KEYBOARD if (-not $kb) { throw "keyboard button not found by label" } [void](Adb-Shell @("input","tap","$($kb[0])","$($kb[1])")) 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) ===" # The IME 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 zh/en toggle is reached by a # calibrated tap. A stray tap here changes a keyboard setting, nothing # destructive. The loop below is idempotent by design (R-9): flip only if # letters-without-Hanzi were seen, and the judgement is the received text. # # Toggle calibration (2026-11-01): the device IME is now SOGOU # (com.sohu.inputmethod.sogou.vivo), not iFlytek - the old (146,2096) iFlytek # coordinate silently missed the key and both attempts stayed in English mode. # Re-measured from screencap: bottom row, the language key right of '.' # (labelled zh/en) at device px (838,2282) on 1080x2400 portrait. $modeToggle = @(838, 2282) $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 ==="