Compare commits
No commits in common. "0089ae86e1c9fa66f62ff5f8a76aa9f9367cd0e2" and "a93430dd3f893a7642888bc16a67dcfe450d432c" have entirely different histories.
0089ae86e1
...
a93430dd3f
@ -276,22 +276,8 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
override fun isReadyForInput(): Boolean = handle != 0L
|
||||
|
||||
/**
|
||||
* 按键转发(物理键盘 / adb 注入 / 无 IME 时的软键盘):
|
||||
* - 返回键:先收 IME,否则断开回表单(阶段 2a 语义延续);
|
||||
* - **可打印**的 `unicodeChar`(0x20 起、排除 0x7F)→ v3 **Text** 通道:
|
||||
* 字符注入与远端键盘布局无关。注意 `getUnicodeChar()` 给的是单个
|
||||
* UTF-16 code unit 而非码点,所以这条路只覆盖 BMP;增补平面字符由
|
||||
* IME `commitText` 成对送达,不经这里;
|
||||
* - 控制值(Enter/Tab/方向键…)与其余控制键 → Set-1 扫描码通道(down/up 配对):
|
||||
* 远端应用期待的是按键事件,不是 WM_CHAR;
|
||||
* - 自动重复(`repeatCount > 0`,长按)只发一次 down:重复由远端键盘驱动,
|
||||
* 本地重发会打爆消息队列。
|
||||
*
|
||||
* 与 IME 路径互斥:IME 显示时按键先被输入法消费(走 commitText),不会到这里;
|
||||
* IME 隐藏时才走本方法。两条路同时生效会重复注入。
|
||||
*
|
||||
* 诊断:vivo logcat 被限流(lessons R-6),路由结果写状态栏,只报 keyCode
|
||||
* 与去向,不报字符内容。
|
||||
* 返回键:软键盘开着时先收键盘(否则一次返回同时关键盘和会话);
|
||||
* 远控态已连接 = 断开回连接界面;连接态 = 交还系统(退出 App)。
|
||||
*/
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
@ -305,44 +291,8 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
disconnect()
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
val h = handle
|
||||
if (h == 0L || event == null) return super.onKeyDown(keyCode, event)
|
||||
// 只报 keyCode 与路由结果,不报字符内容(诊断通道会显示在屏幕上)
|
||||
val cp = event.unicodeChar
|
||||
if (cp >= 0x20 && cp != 0x7F) {
|
||||
if (event.repeatCount == 0) RustBridge.nativeSendText(h, charArrayOf(cp.toChar()))
|
||||
lastTouchDbg = "key code=$keyCode -> text 1u"
|
||||
runOnUiThread { refreshStatus() }
|
||||
return true
|
||||
}
|
||||
val key = WinScancode.of(keyCode) ?: run {
|
||||
lastTouchDbg = "key code=$keyCode 未映射"
|
||||
runOnUiThread { refreshStatus() }
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
if (event.repeatCount == 0) {
|
||||
RustBridge.nativeSendKey(h, key.scancode, if (key.extended) 1 else 0, 1)
|
||||
}
|
||||
lastTouchDbg = "key code=$keyCode -> scan ${key.scancode} ext=${key.extended}"
|
||||
runOnUiThread { refreshStatus() }
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
val h = handle
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK || h == 0L || event == null) {
|
||||
return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
// 字符事件没有「抬起」语义(Text 通道是整段提交),只回扫控制键。
|
||||
// 门槛与 onKeyDown 一致:不可打印的 unicodeChar(Enter/Tab)仍需发 key up,
|
||||
// 否则 onKeyDown 发了 down、这里不发 up,远端会留下按住未释放的键。
|
||||
val cp = event.unicodeChar
|
||||
if (cp >= 0x20 && cp != 0x7F) return true
|
||||
val key = WinScancode.of(keyCode) ?: return super.onKeyUp(keyCode, event)
|
||||
RustBridge.nativeSendKey(h, key.scancode, if (key.extended) 1 else 0, 0)
|
||||
return true
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
// ---- 连接 / 解码 ----
|
||||
|
||||
@ -43,19 +43,6 @@ class RemoteInputConnection(
|
||||
}
|
||||
return true
|
||||
}
|
||||
// 字符事件优先于扫描码:多数软键盘发的是 keyCode=KEYCODE_UNKNOWN +
|
||||
// unicodeChar(adb `input text` 同款形态)。这一分支必须排在 WinScancode
|
||||
// 查表**之前**——否则 UNKNOWN(0) 查不到表会被当「未映射」吞掉,字就丢了。
|
||||
// 注意 `getUnicodeChar()` 返回的是单个 UTF-16 code unit(不是码点),
|
||||
// 所以这条路只覆盖 BMP;增补平面字符由 `commitText` 成对送达。
|
||||
// 只收**可打印**字符(空格起、排除 0x7F):控制值(Enter/Tab)必须走扫描码,
|
||||
// 远端应用期待的是按键事件而非 WM_CHAR。见 MainActivity.onKeyDown 同款门槛。
|
||||
val cp = event.unicodeChar
|
||||
if (event.action == KeyEvent.ACTION_DOWN && cp >= 0x20 && cp != 0x7F) {
|
||||
sendText(charArrayOf(cp.toChar()))
|
||||
debug("keychar 1u")
|
||||
return true
|
||||
}
|
||||
val key = WinScancode.of(event.keyCode)
|
||||
if (key == null) {
|
||||
debug("IME 未映射键 code=${event.keyCode}")
|
||||
|
||||
@ -1,143 +0,0 @@
|
||||
# 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, '<node[^>]*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
|
||||
}
|
||||
@ -1,182 +0,0 @@
|
||||
# 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, '<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 ==="
|
||||
@ -1,259 +0,0 @@
|
||||
# 2b full-chain proof (phone side): Android key -> v3 Text channel -> agent
|
||||
# SendInput -> dedicated Windows target window, read back from that window.
|
||||
# ASCII-only source (PS 5.1 + BOM-less = GBK corruption; see personal lessons).
|
||||
# State checks use ASCII markers ("RTT=" / EditText presence) rather than
|
||||
# matching the app's Chinese labels, for the same reason.
|
||||
#
|
||||
# Why ONE atomic script: SendInput lands on the FOREGROUND window, and Windows'
|
||||
# foreground lock means a background process cannot simply reclaim it. This
|
||||
# box's interactive GUI steals foreground between separate tool calls, so
|
||||
# launch -> assert foreground -> drive phone -> read back must run without
|
||||
# yielding. Every injection is preceded by a foreground assertion: if the target
|
||||
# lost foreground we ABORT rather than inject into some other application.
|
||||
#
|
||||
# Prereqs: vivo device online via adb, app installed, v3 agent build present.
|
||||
# Usage:
|
||||
# powershell -ExecutionPolicy Bypass -File tools\phone_chain_proof.ps1
|
||||
|
||||
param(
|
||||
[string]$PhoneIp = "192.168.1.180",
|
||||
[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-phone-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 FgApi {
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||
}
|
||||
"@
|
||||
|
||||
function Adb {
|
||||
param([Parameter(Mandatory)][string[]]$AdbArgs)
|
||||
# adb writes progress ("1 file pulled...") to stderr; PS 5.1 with
|
||||
# $ErrorActionPreference=Stop turns every stderr line into a terminating
|
||||
# error. Same workaround build-android.ps1 documents: relax for native
|
||||
# calls, judge by exit code and captured stdout.
|
||||
$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) {
|
||||
# Bounded wait, not a single instant check: the target re-asserts itself
|
||||
# every 60ms in proof mode, so a momentary steal by the interactive desktop
|
||||
# is recoverable. Still refuse to inject if it never comes back - a stray
|
||||
# injection lands in whatever window DOES hold foreground.
|
||||
$deadline = (Get-Date).AddMilliseconds(1500)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if ([FgApi]::GetForegroundWindow() -eq $hwnd) { return }
|
||||
Start-Sleep -Milliseconds 30
|
||||
}
|
||||
throw "$where : foreground=$([FgApi]::GetForegroundWindow()) want=$hwnd after 1.5s - refusing to inject"
|
||||
}
|
||||
|
||||
function Read-Received {
|
||||
Wait-File $resultFile 8000
|
||||
$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 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 Wait-For-LiveSession([int]$Seconds, [ref]$outXml) {
|
||||
$deadline = (Get-Date).AddSeconds($Seconds)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Milliseconds 900
|
||||
$x = Get-PhoneUiText
|
||||
if ($x -match "RTT=") { $outXml.Value = $x; return $true }
|
||||
}
|
||||
$outXml.Value = (Get-PhoneUiText)
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-Host "=== 0. stale-agent guard (same rule as the desktop proof) ==="
|
||||
$agentExe = "$repo\target\release\agent.exe"
|
||||
$ag = Get-Process agent -ErrorAction SilentlyContinue
|
||||
if ($ag -and (Get-Item $agentExe).LastWriteTime -gt $ag.StartTime) {
|
||||
Write-Host " running agent predates current build; restarting it"
|
||||
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 (v3)"
|
||||
}
|
||||
|
||||
Write-Host "=== 1. launch target window (own process, empty field) ==="
|
||||
# Retire leftovers from earlier runs WITHOUT killing them. Killing a foreground
|
||||
# window hands foreground to the next app (this box's interactive GUI), and the
|
||||
# foreground lock then refuses a brand-new background process any claim - the
|
||||
# new target could never take foreground back.
|
||||
# Signal only stop files that ALREADY EXIST: creating a fresh one here would
|
||||
# hit the stop path of the run we are about to start, and that target would
|
||||
# close itself the moment it launched (seen 2026-10-30: pid reported then gone).
|
||||
$existingFlags = @(Get-ChildItem -Path $env:TEMP -Recurse -Filter "stop.flag" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -like "*remote*proof*" })
|
||||
if ($existingFlags.Count -gt 0) {
|
||||
Write-Host " retiring $($existingFlags.Count) earlier target(s) via their own stop file"
|
||||
foreach ($f in $existingFlags) { (Get-Item $f.FullName).LastWriteTime = Get-Date }
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
Start-Process powershell.exe -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass",`
|
||||
"-File","$repo\tools\text_target.ps1","-PidFile",$pidFile,`
|
||||
"-ResultFile",$resultFile,"-StopFile",$stopFile,"-GrabForeground"
|
||||
Wait-File $pidFile
|
||||
# pid file carries "pid<n><hwnd>": the target writes its own handle once the
|
||||
# form is realized, which removes the Get-Process MainWindowHandle race.
|
||||
$parts = @((Get-Content $pidFile -Raw) -split "`n")
|
||||
$tid = [int]$parts[0].Trim()
|
||||
$hwnd = [IntPtr][long]$parts[1].Trim()
|
||||
if ($hwnd -eq 0) { throw "target reported a null window handle" }
|
||||
Assert-Foreground $hwnd "after launch"
|
||||
Write-Host " target pid=$tid hwnd=$($hwnd.ToInt64()) foreground=yes"
|
||||
|
||||
Write-Host "=== 2. start app, connect via intent extras ==="
|
||||
[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))
|
||||
$xmlRef = ""
|
||||
if (-not (Wait-For-LiveSession 25 ([ref]$xmlRef))) { throw "phone never reported a live session (no RTT marker)" }
|
||||
$script:xml = $xmlRef
|
||||
Write-Host " phone: $([regex]::Match($script:xml,'text="([^"]*RTT=[^"]*)"').Groups[1].Value)"
|
||||
Assert-Foreground $hwnd "after connect"
|
||||
|
||||
Write-Host "=== 3. phone keys A B C -> Activity.onKeyDown -> v3 Text channel ==="
|
||||
# 29/30/31 = KEYCODE_A/B/C. adb-injected keys arrive with a printable
|
||||
# unicodeChar, so the app routes them to nativeSendText (IME hidden). That is
|
||||
# the product path introduced in 2b, not a stand-in for it.
|
||||
foreach ($k in 29,30,31) {
|
||||
Assert-Foreground $hwnd "before keyevent $k"
|
||||
[void](Adb-Shell @("input", "keyevent", "$k"))
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
$deadline = (Get-Date).AddSeconds(10)
|
||||
$got = ""
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$got = Read-Received
|
||||
if ($got -eq "abc") { break }
|
||||
Start-Sleep -Milliseconds 300
|
||||
}
|
||||
Write-Host " target received: [$got]"
|
||||
if ($got -ne "abc") {
|
||||
Write-Host " foreground now: $([FgApi]::GetForegroundWindow()) target: $hwnd"
|
||||
throw "FULL-CHAIN FAIL: expected [abc] got [$got]"
|
||||
}
|
||||
Write-Host " PASS: Android key -> v3 Text -> agent KEYEVENTF_UNICODE -> Windows window"
|
||||
|
||||
Write-Host "=== 4. phone ENTER (66): control key uses the scancode channel ==="
|
||||
# Enter carries no printable unicodeChar, so it must go down the WinScancode
|
||||
# path. Observable acceptance: session survives and keeps streaming (a
|
||||
# single-line TextBox swallows Enter, so glyph-level proof is not possible).
|
||||
Assert-Foreground $hwnd "before ENTER"
|
||||
[void](Adb-Shell @("input", "keyevent", "66"))
|
||||
Start-Sleep -Milliseconds 900
|
||||
$xmlRef = ""
|
||||
if (-not (Wait-For-LiveSession 6 ([ref]$xmlRef))) { throw "session died after ENTER" }
|
||||
Write-Host " session alive: $([regex]::Match($xmlRef,'text="([^"]*RTT=[^"]*)"').Groups[1].Value)"
|
||||
|
||||
Write-Host "=== 5. phone BACK (4): disconnect and return to form ==="
|
||||
[void](Adb-Shell @("input", "keyevent", "4"))
|
||||
Start-Sleep -Seconds 2
|
||||
$xml2 = Get-PhoneUiText
|
||||
if ($xml2 -match "RTT=") { throw "BACK did not disconnect (still live session)" }
|
||||
if ($xml2 -notmatch "android\.widget\.EditText") { throw "BACK did not return to the form" }
|
||||
Write-Host " PASS: disconnected, form visible again"
|
||||
|
||||
Write-Host "=== 6. connect again FROM THE FORM (V4 through real UI, not intent) ==="
|
||||
# Form typing uses keyevent, not 'adb input text': with the Chinese IME active,
|
||||
# text injection gets composed (ASCII '.' becomes U+3002). We disable the IME,
|
||||
# restore it in finally, and never assume a Chinese IME is absent.
|
||||
$origIme = (Adb-Shell @("settings", "get", "secure", "default_input_method")).out.Trim()
|
||||
Write-Host " original IME: [$origIme]"
|
||||
if ($origIme -and $origIme -ne "null") { [void](Adb-Shell @("ime", "disable", $origIme)) }
|
||||
try {
|
||||
$xml3 = Get-PhoneUiText
|
||||
$fields = [regex]::Matches($xml3, '<node[^>]*class="android\.widget\.EditText"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
if ($fields.Count -lt 3) { throw "expected 3 form fields, found $($fields.Count)" }
|
||||
function Center($m) {
|
||||
$a = [int]$m.Groups[1].Value; $b = [int]$m.Groups[2].Value
|
||||
$c = [int]$m.Groups[3].Value; $d = [int]$m.Groups[4].Value
|
||||
return @(([int](($a + $c) / 2)), ([int](($b + $d) / 2)))
|
||||
}
|
||||
$ipC = Center $fields[0]; $codeC = Center $fields[2]
|
||||
# KEYCODE_0=7..KEYCODE_9=16, PERIOD=56, DEL=67, T=48 (A=29 -> T=29+19;
|
||||
# all from javap against android-34, not memory)
|
||||
[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"))
|
||||
}
|
||||
[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")) }
|
||||
[void](Adb-Shell @("input", "keyevent", "48"))
|
||||
$xml3 = Get-PhoneUiText
|
||||
$vals = [regex]::Matches($xml3, '<node[^>]*class="android\.widget\.EditText"[^>]*text="([^"]*)"') |
|
||||
ForEach-Object { $_.Groups[1].Value }
|
||||
Write-Host " form now: $($vals -join ' | ')"
|
||||
$btn = [regex]::Match($xml3, '<node[^>]*class="android\.widget\.Button"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
if (-not $btn.Success) { throw "connect button not found" }
|
||||
$bc = Center $btn
|
||||
Assert-Foreground $hwnd "before form connect"
|
||||
[void](Adb-Shell @("input", "tap", "$($bc[0])", "$($bc[1])"))
|
||||
$xmlRef = ""
|
||||
if (-not (Wait-For-LiveSession 25 ([ref]$xmlRef))) {
|
||||
$shown = [regex]::Matches($xmlRef, 'text="([^"]+)"') | ForEach-Object { $_.Groups[1].Value }
|
||||
throw "V4 FAIL: form connect did not reach a live session. screen=[$($shown -join ' | ')]"
|
||||
}
|
||||
Write-Host " V4 PASS via form UI: $([regex]::Match($xmlRef,'text="([^"]*RTT=[^"]*)"').Groups[1].Value)"
|
||||
}
|
||||
finally {
|
||||
if ($origIme -and $origIme -ne "null") {
|
||||
[void](Adb-Shell @("ime", "enable", $origIme))
|
||||
[void](Adb-Shell @("ime", "set", $origIme))
|
||||
Write-Host " IME restored: $((Adb-Shell @("settings", "get", "secure", "default_input_method")).out.Trim())"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "=== 7. stop target ==="
|
||||
New-Item -ItemType File -Force -Path $stopFile | Out-Null
|
||||
Start-Sleep -Milliseconds 600
|
||||
Write-Host "=== PHONE CHAIN PROOF COMPLETE ==="
|
||||
@ -1,23 +0,0 @@
|
||||
# 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)
|
||||
@ -114,13 +114,11 @@ Start-Process powershell.exe -ArgumentList "-NoProfile","-ExecutionPolicy","Bypa
|
||||
"-File","$repo\tools\text_target.ps1","-PidFile",$pidFile,`
|
||||
"-ResultFile",$resultFile,"-StopFile",$stopFile
|
||||
Wait-File $pidFile
|
||||
# target.pid holds "pid<n><hwnd>" (see text_target.ps1 Shown handler); take line 1.
|
||||
$parts = @((Get-Content $pidFile -Raw) -split "`n")
|
||||
$tid = [int]$parts[0].Trim()
|
||||
$tid = [int](Get-Content $pidFile -Raw).Trim()
|
||||
$tp = Get-Process -Id $tid
|
||||
Start-Sleep -Milliseconds 1200
|
||||
$hwnd = [IntPtr][long]$parts[1].Trim()
|
||||
if ($hwnd -eq 0) { throw "target reported a null window handle" }
|
||||
$hwnd = $tp.MainWindowHandle
|
||||
if ($hwnd -eq 0) { throw "target has no window handle" }
|
||||
Write-Host " target pid=$tid hwnd=$hwnd title=[$($tp.MainWindowTitle)]"
|
||||
Assert-TargetForeground $hwnd
|
||||
Write-Host " target is foreground: yes"
|
||||
|
||||
@ -10,13 +10,7 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$PidFile,
|
||||
[Parameter(Mandatory = $true)][string]$ResultFile,
|
||||
[Parameter(Mandatory = $true)][string]$StopFile,
|
||||
# SendInput lands on whatever is FOREGROUND. When the injector is driven
|
||||
# from another machine (phone -> agent -> this desktop), nothing in this
|
||||
# process keeps our window on top, so an unrelated window could eat the
|
||||
# injection. With this switch the form re-asserts foreground every tick and
|
||||
# the caller still asserts foreground before injecting.
|
||||
[switch]$GrabForeground
|
||||
[Parameter(Mandatory = $true)][string]$StopFile
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
@ -42,37 +36,6 @@ $tb.Font = New-Object System.Drawing.Font("Segoe UI", 16.0)
|
||||
$tb.Dock = "Fill"
|
||||
$form.Controls.Add($tb)
|
||||
|
||||
$script:GrabForeground = $GrabForeground
|
||||
if ($script:GrabForeground) {
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class OwnForeground {
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, IntPtr pid);
|
||||
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
||||
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
|
||||
|
||||
// Deliberately NO keybd_event(VK_MENU). The usual 'tap ALT to beat the
|
||||
// foreground lock' trick corrupts exactly what we measure: after ALT, Windows
|
||||
// treats the next character as a menu MNEMONIC, so the first injected
|
||||
// character is eaten. Measured 2026-10-30: ALT re-asserting every 60ms turned
|
||||
// three injected characters into only 'b' in the target.
|
||||
// Attaching input queues is a lock workaround that synthesises no key input.
|
||||
public static bool Claim(IntPtr h) {
|
||||
uint fgThread = GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero);
|
||||
uint myThread = GetCurrentThreadId();
|
||||
AttachThreadInput(myThread, fgThread, true);
|
||||
bool ok = SetForegroundWindow(h);
|
||||
AttachThreadInput(myThread, fgThread, false);
|
||||
return ok && GetForegroundWindow() == h;
|
||||
}
|
||||
public static IntPtr Now() { return GetForegroundWindow(); }
|
||||
}
|
||||
"@
|
||||
}
|
||||
|
||||
$timer = New-Object System.Windows.Forms.Timer
|
||||
$timer.Interval = 150
|
||||
$timer.Add_Tick({
|
||||
@ -80,14 +43,6 @@ $timer.Add_Tick({
|
||||
$form.Close()
|
||||
return
|
||||
}
|
||||
if ($script:GrabForeground) {
|
||||
# Re-assert only when actually lost (unconditional claiming every tick
|
||||
# would fight the user for the desktop outside a proof run).
|
||||
if ([OwnForeground]::Now() -ne $form.Handle) {
|
||||
[void]$form.Activate()
|
||||
[void][OwnForeground]::Claim($form.Handle)
|
||||
}
|
||||
}
|
||||
if ($tb.Text -ne $script:lastWritten) {
|
||||
$script:lastWritten = $tb.Text
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
@ -99,10 +54,7 @@ $timer.Add_Tick({
|
||||
$form.Add_Shown({
|
||||
$form.Activate()
|
||||
[void]$tb.Focus()
|
||||
# Report BOTH pid and window handle: a proof caller resolving the handle via
|
||||
# Get-Process races with window creation and can read 0. Handle is stable
|
||||
# here because the form is realized by the time Shown fires.
|
||||
[System.IO.File]::WriteAllText($script:PidFile, "$PID`n" + $form.Handle.ToInt64())
|
||||
[System.IO.File]::WriteAllText($script:PidFile, "$PID")
|
||||
})
|
||||
|
||||
$timer.Start()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user