2026-10-30 01:06:08 -04:00
|
|
|
# 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(
|
2026-10-31 03:57:57 -04:00
|
|
|
# PC (controlled side) LAN address; DHCP-assigned - pass -PhoneIp when it
|
|
|
|
|
# moves (see ime_chinese_proof.ps1 for the 2026-10-31 stale-default lesson).
|
|
|
|
|
[string]$PhoneIp = "192.168.1.110",
|
2026-10-30 01:06:08 -04:00
|
|
|
[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, '<node[^>]*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 ==="
|