test(tools): 4a 横屏验收脚本 + 既有 proof 适配自动隐藏与同源缺陷修复
新增 landscape_proof.ps1:V-C1~C7 判据(自动横屏 <=2s、旋转零中断、 动画桌面下 >=45fps、控制条自动隐藏、黑边唤出零远端泄漏、四按钮可点、 方向四态循环、表单竖屏)。方向判定只解析 Display: mDisplayId=0 块内 cur=WxH——这台 vivo 无 mRotation= 行,且 dumpsys 里有常驻 1080x2400 的 虚拟屏,匹配首个 cur= 会在已横屏时误报未旋转(探针实测踩过)。 横屏自动隐藏与既有 proof 的判据自冲突(HUD 随控制条 GONE,RTT= 读不到 被误判断线/未连接),三个 proof 统一加「确认隐藏态才走黑边唤醒」: phone_chain_proof、ime_chinese_proof、rel_mouse_phone_proof。 修 rel_mouse_phone_proof 两处: - 2c 基线判据:横屏滑动 768px 的残余相对事件成批延迟落地,旧的「两次 读数相等」被突发间隙骗过(实测假失败 351/1418px)。改为 6 读数沉降 + 4 次采样判漂移 + 绝对泄漏点特征比对。 - step 4 靶窗启动缺 3 个 Mandatory 参数,子进程瞬间失败退出(3b 起一直 如此,右键落在裸屏幕中心,靠近违反 R-11「不得注入他人窗口」)。现按 契约启动并把指针停在自家窗口内。 R-5 同源排查:靶窗结果目录固定路径 + 启动时删自己的 stop.flag,会让 上一轮失败遗留的靶窗永远收不到退休信号、继续抢前台并写脏 received.txt (实测收到 abc\nabc)。5 个 proof 统一改每轮唯一目录(\47512 后缀)。 ime_chinese_proof 的切换键标定失效:设备 IME 已换成搜狗(原为讯飞), 旧坐标 (146,2096) 静默点空。IME 按键视图对 uiautomator 不可见、无法机械 标定,按 R-9 转人工确认,不硬凑自动化。
This commit is contained in:
parent
cff7fc8ab3
commit
e96502f888
@ -26,7 +26,11 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repo = (Resolve-Path "$PSScriptRoot\..").Path
|
||||
$adb = "D:\working\android\sdk\platform-tools\adb.exe"
|
||||
$tmp = "$env:TEMP\remote-emoji-proof"
|
||||
# UNIQUE per run: a fixed path lets the startup Remove-Item below delete the
|
||||
# stop.flag that leftover targets from a previous FAILED run are watching, so
|
||||
# they never retire and keep writing into the shared received.txt
|
||||
# (same trap fixed in phone_chain_proof, measured 2026-11-01).
|
||||
$tmp = "$env:TEMP\remote-emoji-proof-$PID"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
||||
$pidFile = "$tmp\target.pid"
|
||||
|
||||
@ -16,17 +16,23 @@
|
||||
# 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",
|
||||
# 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"
|
||||
$tmp = "$env:TEMP\remote-cjk2"
|
||||
# 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"
|
||||
@ -99,6 +105,86 @@ 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,'<node[^>]*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*" })
|
||||
@ -138,10 +224,11 @@ 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 } }
|
||||
$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.
|
||||
@ -150,19 +237,7 @@ Write-Host "=== 4. show IME (tap keyboard button) ==="
|
||||
# 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 = $null
|
||||
foreach ($n in [regex]::Matches($ui,'<node[^>]*class="android\.widget\.Button"[^>]*>')) {
|
||||
$t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
|
||||
if ($t -eq $F_KEYBOARD) {
|
||||
$b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
if ($b.Success) {
|
||||
$kb = @(
|
||||
[int](([int]$b.Groups[1].Value + [int]$b.Groups[3].Value) / 2),
|
||||
[int](([int]$b.Groups[2].Value + [int]$b.Groups[4].Value) / 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
$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
|
||||
@ -172,16 +247,20 @@ if ($ime -notmatch "mHaveConnection=true") { throw "IME not bound to our InputCo
|
||||
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)
|
||||
# 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 = ""
|
||||
|
||||
@ -26,7 +26,11 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repo = (Resolve-Path "$PSScriptRoot\..").Path
|
||||
$adb = "D:\working\android\sdk\platform-tools\adb.exe"
|
||||
$tmp = "$env:TEMP\remote-ime-proof"
|
||||
# UNIQUE per run: a fixed path lets the startup Remove-Item below delete the
|
||||
# stop.flag that leftover targets from a previous FAILED run are watching, so
|
||||
# they never retire and keep writing into the shared received.txt
|
||||
# (same trap fixed in phone_chain_proof, measured 2026-11-01).
|
||||
$tmp = "$env:TEMP\remote-ime-proof-$PID"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
||||
$pidFile = "$tmp\target.pid"
|
||||
|
||||
486
tools/landscape_proof.ps1
Normal file
486
tools/landscape_proof.ps1
Normal file
@ -0,0 +1,486 @@
|
||||
# 4a landscape proof: rotation, full-height video, auto-hide controls, regressions.
|
||||
#
|
||||
# ASCII-only (PS 5.1 reads BOM-less .ps1 as GBK; personal lessons).
|
||||
#
|
||||
# Judgements (loop-4a-landscape-plan.md section 6):
|
||||
# V-C1 connect to 1920x1080 -> app rotates landscape <=2s after live
|
||||
# (LOCAL display cur= only: this vivo ROM has a permanent 1080x2400
|
||||
# virtual display and prints no mRotation= line - probe lesson 2026-11-01)
|
||||
# V-C2 with controls hidden, video view is 16:9 (err <2%) and >=90% screen height
|
||||
# V-C3 frame counter strictly increases across rotation (session survives)
|
||||
# V-C4 >=45 fps over a 2.5s window with an ACTIVE desktop animator
|
||||
# (static desktop is source-limited ~25fps - measuring that would be a
|
||||
# category error, probe lesson)
|
||||
# V-C5 controls auto-hide ~3s idle; bottom-edge tap wakes them back
|
||||
# V-C5b edge-band tap does NOT reach the remote (PC cursor delta <=1px)
|
||||
# V-C5c all four buttons present after wake; mode tap changes and restores
|
||||
# V-C6 form page is portrait
|
||||
# V-C7 orientation button cycles AUTO->L->P->SYS->AUTO, display follows
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File tools\landscape_proof.ps1 -Host_ 192.168.1.112
|
||||
param(
|
||||
[string]$Host_ = "192.168.1.112",
|
||||
[int]$Port = 47800,
|
||||
[string]$Code = "t",
|
||||
[string]$AgentExe = ""
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repo = (Resolve-Path "$PSScriptRoot\..").Path
|
||||
$adb = "D:\working\android\sdk\platform-tools\adb.exe"
|
||||
$ev = Join-Path (Split-Path $repo -Parent) "evidence\4a"
|
||||
New-Item -ItemType Directory -Force -Path $ev | Out-Null
|
||||
|
||||
# ---- Chinese fragments as code points (R-8) ----
|
||||
$F_ORIENT = -join ([char]0x65B9,[char]0x5411,[char]0x3A) # "fangxiang:" (orientation:)
|
||||
$F_AUTO = $F_ORIENT + -join ([char]0x81EA,[char]0x52A8) # label: orientation AUTO
|
||||
$F_LAND = $F_ORIENT + -join ([char]0x6A2A,[char]0x5C4F) # label: orientation LANDSCAPE
|
||||
$F_PORT = $F_ORIENT + -join ([char]0x7AD6,[char]0x5C4F) # label: orientation PORTRAIT
|
||||
$F_SYS = $F_ORIENT + -join ([char]0x7CFB,[char]0x7EDF) # label: orientation SYSTEM
|
||||
$F_CONNECT = -join ([char]0x8FDE,[char]0x63A5) # label: connect
|
||||
$F_RECONNECT = -join ([char]0x91CD,[char]0x65B0,[char]0x8FDE,[char]0x63A5) # label: reconnect
|
||||
$F_KB = -join ([char]0x952E,[char]0x76D8) # label: keyboard
|
||||
$F_MODE = -join ([char]0x6A21,[char]0x5F0F,[char]0x3A) # "moshi:" (mode:)
|
||||
$F_DISC = -join ([char]0x65AD,[char]0x5F00) # label: disconnect
|
||||
|
||||
function Adb {
|
||||
param([Parameter(Mandatory)][string[]]$AdbArgs)
|
||||
$old = $ErrorActionPreference; $ErrorActionPreference = "Continue"
|
||||
try { $out = & $adb @AdbArgs 2>$null; @{ out = ($out -join "`n"); code = $LASTEXITCODE } }
|
||||
finally { $ErrorActionPreference = $old }
|
||||
}
|
||||
function AdbShell { param([string[]]$Cmd) (Adb -AdbArgs (@("shell") + $Cmd)).out }
|
||||
|
||||
function Get-Ui {
|
||||
[void](AdbShell @("uiautomator","dump","/sdcard/ls.xml"))
|
||||
$r = Adb -AdbArgs @("pull","/sdcard/ls.xml","$env:TEMP\ls.xml")
|
||||
if ($r.code -ne 0 -or -not (Test-Path "$env:TEMP\ls.xml")) { return "" }
|
||||
return [System.IO.File]::ReadAllText("$env:TEMP\ls.xml", [System.Text.Encoding]::UTF8)
|
||||
}
|
||||
# uiautomator intermittently returns an empty/failed dump (measured 2026-11-01:
|
||||
# a tap lookup got "" and the null-button tap then crashed with an empty-string
|
||||
# argument). Retry until the tree is plausibly real; callers still assert on
|
||||
# their own node - this only removes the transient, never masks a real absence.
|
||||
function Get-UiRetry([int]$tries=4) {
|
||||
$x = ""
|
||||
for ($i=0; $i -lt $tries; $i++) {
|
||||
$x = Get-Ui
|
||||
if ($x.Length -gt 500) { return $x }
|
||||
Start-Sleep -Milliseconds 400
|
||||
}
|
||||
Write-Host " [ui] retry exhausted, last dump len=$($x.Length)"
|
||||
return $x
|
||||
}
|
||||
function VisibleTexts([string]$xml) {
|
||||
[regex]::Matches($xml,'text="([^"]+)"') | ForEach-Object { [System.Net.WebUtility]::HtmlDecode($_.Groups[1].Value) }
|
||||
}
|
||||
function ButtonByName([string]$xml, [string]$want, [bool]$prefix=$false) {
|
||||
foreach ($n in [regex]::Matches($xml,'<node[^>]*class="android\.widget\.Button"[^>]*>')) {
|
||||
$t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
|
||||
$hit = if ($prefix) { $t.StartsWith($want) } else { $t -eq $want }
|
||||
if ($hit) {
|
||||
$b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
if ($b.Success) {
|
||||
$x1=[int]$b.Groups[1].Value; $y1=[int]$b.Groups[2].Value
|
||||
$x2=[int]$b.Groups[3].Value; $y2=[int]$b.Groups[4].Value
|
||||
return @{ cx=[int](($x1+$x2)/2); cy=[int](($y1+$y2)/2) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function ButtonLabelByPrefix([string]$xml, [string]$prefix) {
|
||||
foreach ($n in [regex]::Matches($xml,'<node[^>]*class="android\.widget\.Button"[^>]*>')) {
|
||||
$t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
|
||||
if ($t.StartsWith($prefix)) { return $t }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
# 4a race (measured 2026-11-01): each uiautomator dump costs ~2s, comparable to
|
||||
# the 3s auto-hide timer - find-then-read sequences straddle expiry and labels
|
||||
# come back empty. Every observation goes through the helpers below.
|
||||
#
|
||||
# CRITICAL SAFETY: the wake tap (screen bottom center) is only consumed by the
|
||||
# app's edge band WHEN CONTROLS ARE HIDDEN. If controls are visible, that point
|
||||
# lands on the control bar (mode/orientation buttons!) and would fire a real
|
||||
# action. So a wake is allowed only after positively proving the hidden state:
|
||||
# video View node present AND zero Button nodes.
|
||||
function ControlsHidden([string]$ui) {
|
||||
$anyBtn = ($ui -match 'class="android\.widget\.Button"')
|
||||
$video = ($ui -match 'class="android\.view\.View"')
|
||||
return ($video -and -not $anyBtn)
|
||||
}
|
||||
function Wake-Band {
|
||||
# 4a design change (measured 2026-11-01): the wake zone is the LETTERBOX
|
||||
# BLACK BAR, not a bottom edge band. With controls hidden the video is
|
||||
# full-height, so bottom-center lies INSIDE the picture and would fire a
|
||||
# real remote click. x=100 is outside the video in both states.
|
||||
$d = LocalDisplay
|
||||
[void](AdbShell @("input","tap","100","$([int]($d.h/2))"))
|
||||
Start-Sleep -Milliseconds 400
|
||||
}
|
||||
function Find-VisibleButton([string]$want, [bool]$prefix=$false) {
|
||||
for ($try=0; $try -lt 5; $try++) {
|
||||
$ui = Get-UiRetry
|
||||
$b = ButtonByName $ui $want $prefix
|
||||
if ($b) { return $b }
|
||||
if (ControlsHidden $ui) { Wake-Band; continue }
|
||||
if ($ui.Length -gt 500) { return $null } # real tree, genuine absence
|
||||
Start-Sleep -Milliseconds 400 # empty-dump transient
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Tap-VisibleButton([string]$want, [bool]$prefix=$false) {
|
||||
$b = Find-VisibleButton $want $prefix
|
||||
if (-not $b) { return $null }
|
||||
[void](AdbShell @("input","tap","$($b.cx)","$($b.cy)"))
|
||||
return $b
|
||||
}
|
||||
# Label read that survives the hide race: wake only from a proven-hidden state.
|
||||
function Read-ButtonLabel([string]$want, [bool]$prefix=$false) {
|
||||
for ($try=0; $try -lt 6; $try++) {
|
||||
$ui = Get-UiRetry
|
||||
$l = ButtonLabelByPrefix $ui $want
|
||||
if ($l) { return $l }
|
||||
if (ControlsHidden $ui) { Wake-Band; continue }
|
||||
if ($ui.Length -gt 500) { return $null }
|
||||
Start-Sleep -Milliseconds 400
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function FrameCount {
|
||||
$m = [regex]::Match((Get-Ui), ([char]0x5E27 + '=(\d+)'))
|
||||
if ($m.Success) { return [long]$m.Groups[1].Value }
|
||||
return -1
|
||||
}
|
||||
# 4a self-conflict (measured 2026-11-01): once the controls auto-hide, the HUD
|
||||
# TextView is GONE too - uiautomator's tree has NO text nodes and FrameCount
|
||||
# returns -1 even though the session is alive (the video View is right there).
|
||||
# Wake through the bottom edge band first: that tap is consumed by the app
|
||||
# (zero remote events), so it cannot perturb the fps window.
|
||||
function WakeControls {
|
||||
$d = LocalDisplay
|
||||
[void](AdbShell @("input","tap","100","$([int]($d.h/2))"))
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
function FrameCountOrWake {
|
||||
$f = FrameCount
|
||||
if ($f -ge 0) { return $f }
|
||||
WakeControls
|
||||
return FrameCount
|
||||
}
|
||||
function LocalDisplay {
|
||||
$d = (AdbShell @("dumpsys","window","displays"))
|
||||
$idx = $d.IndexOf("Display: mDisplayId=0")
|
||||
if ($idx -lt 0) { return @{ w=0; h=0 } }
|
||||
$m = [regex]::Match($d.Substring($idx), 'cur=(\d+)x(\d+)')
|
||||
if (-not $m.Success) { return @{ w=0; h=0 } }
|
||||
return @{ w=[int]$m.Groups[1].Value; h=[int]$m.Groups[2].Value }
|
||||
}
|
||||
function Wake {
|
||||
[void](AdbShell @("input","keyevent","224"))
|
||||
Start-Sleep -Milliseconds 250
|
||||
[void](AdbShell @("wm","dismiss-keyguard"))
|
||||
Start-Sleep -Milliseconds 350
|
||||
}
|
||||
function Restart-AppForm {
|
||||
Wake
|
||||
[void](AdbShell @("am","force-stop","com.verify.remote"))
|
||||
Start-Sleep -Milliseconds 600
|
||||
[void](AdbShell @("am","start","-n","com.verify.remote/.MainActivity"))
|
||||
$deadline = (Get-Date).AddSeconds(8)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$x = Get-Ui
|
||||
if (([regex]::Matches($x,'class="android\.widget\.EditText"')).Count -ge 3) { return $x }
|
||||
Start-Sleep -Milliseconds 300
|
||||
}
|
||||
throw "form did not appear: [$((VisibleTexts (Get-Ui)) -join ' || ')]"
|
||||
}
|
||||
function CenterOf($m) {
|
||||
$a=[int]$m.Groups[1].Value; $b=[int]$m.Groups[2].Value
|
||||
$c=[int]$m.Groups[3].Value; $e=[int]$m.Groups[4].Value
|
||||
@([int](($a+$c)/2), [int](($b+$e)/2))
|
||||
}
|
||||
function TypeAscii([string]$s) {
|
||||
foreach ($ch in $s.ToUpper().ToCharArray()) {
|
||||
$k = if ($ch -ge '0' -and $ch -le '9') { 7 + [int][string]$ch }
|
||||
elseif ($ch -ge 'A' -and $ch -le 'Z') { 29 + ([int]$ch - [int][char]'A') }
|
||||
elseif ($ch -eq '.') { 56 } else { throw "unsupported char $ch" }
|
||||
[void](AdbShell @("input","keyevent","$k"))
|
||||
Start-Sleep -Milliseconds 70
|
||||
}
|
||||
}
|
||||
function ClearField($c) {
|
||||
for ($try=0; $try -lt 3; $try++) {
|
||||
[void](AdbShell @("input","tap","$($c[0])","$($c[1])"))
|
||||
Start-Sleep -Milliseconds 250
|
||||
for ($i=0; $i -lt 40; $i++) { [void](AdbShell @("input","keyevent","67")) }
|
||||
Start-Sleep -Milliseconds 120
|
||||
$fx = Get-Ui
|
||||
$focused = $false
|
||||
$fi = -1
|
||||
foreach ($n in [regex]::Matches($fx,'<node[^>]*>')) {
|
||||
if ($n.Value -match 'class="android\.widget\.EditText"') {
|
||||
$fi++
|
||||
if ($n.Value -match 'focused="true"') { $focused = $true }
|
||||
}
|
||||
}
|
||||
if ($focused) { return }
|
||||
}
|
||||
throw "field focus failed"
|
||||
}
|
||||
|
||||
# PC-side cursor (V-C5b) + desktop animator (V-C4)
|
||||
Add-Type -TypeDefinition @"
|
||||
using System; using System.Runtime.InteropServices;
|
||||
public class Cur4a {
|
||||
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; }
|
||||
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT p);
|
||||
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
|
||||
[DllImport("user32.dll")] public static extern bool SetProcessDpiAwarenessContext(IntPtr c);
|
||||
public static void Dpi(){ SetProcessDpiAwarenessContext(new IntPtr(-4)); }
|
||||
public static int[] Get(){ POINT p; GetCursorPos(out p); return new int[]{p.X,p.Y}; }
|
||||
public static void Set(int x,int y){ SetCursorPos(x,y); }
|
||||
}
|
||||
"@
|
||||
[Cur4a]::Dpi()
|
||||
|
||||
Write-Host "=== 0. memory precheck (probe lesson: OOM fakes failures) ==="
|
||||
$os = Get-CimInstance Win32_OperatingSystem
|
||||
$freeGB = $os.FreeVirtualMemory/1MB
|
||||
Write-Host (" commit free: {0:N1} GB" -f $freeGB)
|
||||
if ($freeGB -lt 0.5) { throw "commit memory critically low - free RAM first (gradle --stop)" }
|
||||
|
||||
Write-Host "=== 1. stale-agent guard (R-7) ==="
|
||||
if (-not $AgentExe) { $AgentExe = Join-Path $repo "target\release\agent.exe" }
|
||||
$ag = @(Get-Process agent -ErrorAction SilentlyContinue)
|
||||
if ($ag.Count -gt 0 -and (Test-Path $AgentExe) -and ((Get-Item $AgentExe).LastWriteTime -gt $ag[0].StartTime)) {
|
||||
Write-Host " agent predates build; restarting"
|
||||
Stop-Process -Name agent -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Milliseconds 800; $ag = @()
|
||||
}
|
||||
if ($ag.Count -eq 0) {
|
||||
Start-Process $AgentExe -ArgumentList "--port","$Port","--code",$Code -WindowStyle Hidden | Out-Null
|
||||
Start-Sleep -Seconds 2; Write-Host " agent started"
|
||||
} else { Write-Host " agent running and current" }
|
||||
|
||||
$origIme = ((AdbShell @("settings","get","secure","default_input_method")) -join "").Trim()
|
||||
if ($origIme -and $origIme -ne "null") { [void](AdbShell @("ime","disable",$origIme)) }
|
||||
[void](AdbShell @("svc","power","stayon","true"))
|
||||
|
||||
try {
|
||||
# =====================================================================
|
||||
Write-Host "=== 2. V-C6: form is portrait ==="
|
||||
$x = Restart-AppForm
|
||||
$d = LocalDisplay
|
||||
Write-Host (" form display: {0}x{1}" -f $d.w, $d.h)
|
||||
if (-not ($d.w -lt $d.h)) { throw "V-C6 FAIL: form not portrait ($($d.w)x$($d.h))" }
|
||||
Write-Host " PASS: form portrait"
|
||||
|
||||
# =====================================================================
|
||||
Write-Host "=== 3. V-C1: connect -> AUTO rotates landscape <=2s after live ==="
|
||||
$F_edits = [regex]::Matches($x,'<node[^>]*class="android\.widget\.EditText"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
ClearField (CenterOf $F_edits[0]); TypeAscii $Host_
|
||||
ClearField (CenterOf $F_edits[1]); TypeAscii "$Port"
|
||||
ClearField (CenterOf $F_edits[2]); TypeAscii $Code
|
||||
# Read back until 3 EditText nodes are actually present: uiautomator
|
||||
# intermittently returns a truncated/empty tree (measured 2026-11-01, worse
|
||||
# under memory pressure) and a single Get-Ui then reads "form now: " empty
|
||||
# and fails the fill check as a false negative.
|
||||
$vals = @()
|
||||
for ($rd=0; $rd -lt 5; $rd++) {
|
||||
$x2 = Get-UiRetry
|
||||
$vals = @()
|
||||
foreach ($n in [regex]::Matches($x2,'<node[^>]*>')) {
|
||||
if ($n.Value -match 'class="android\.widget\.EditText"') {
|
||||
$vals += [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
|
||||
}
|
||||
}
|
||||
if ($vals.Count -ge 3) { break }
|
||||
Write-Host " form readback got $($vals.Count) fields, retry $rd"
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
Write-Host " form now: $($vals -join ' | ')"
|
||||
if ($vals.Count -lt 3) { throw "form fill FAIL: only $($vals.Count) fields after retries" }
|
||||
if ($vals[0] -ne $Host_ -or $vals[1] -ne "$Port" -or $vals[2] -ne $Code) { throw "form fill FAIL" }
|
||||
$btn = ButtonByName $x2 $F_CONNECT
|
||||
if (-not $btn) { $btn = ButtonByName $x2 $F_RECONNECT }
|
||||
if (-not $btn) { $btn = Find-VisibleButton $F_CONNECT; if (-not $btn) { $btn = Find-VisibleButton $F_RECONNECT } }
|
||||
if (-not $btn) { throw "connect button not found" }
|
||||
[void](AdbShell @("input","tap","$($btn.cx)","$($btn.cy)"))
|
||||
$deadline = (Get-Date).AddSeconds(20); $live = $false
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Milliseconds 500
|
||||
if ((Get-Ui) -match "RTT=") { $live = $true; break }
|
||||
}
|
||||
if (-not $live) { throw "never reached live: [$((VisibleTexts (Get-Ui)) -join ' || ')]" }
|
||||
$tRot = Get-Date
|
||||
$deadline = $tRot.AddSeconds(2); $rotated = $false
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$d = LocalDisplay
|
||||
if ($d.w -gt $d.h) { $rotated = $true; break }
|
||||
Start-Sleep -Milliseconds 150
|
||||
}
|
||||
$ms = ((Get-Date) - $tRot).TotalMilliseconds
|
||||
Write-Host (" display {0}x{1}, landscape within {2:N0}ms of live" -f $d.w,$d.h,$ms)
|
||||
if (-not $rotated) { throw "V-C1 FAIL: still portrait 2s after live" }
|
||||
Write-Host " PASS: AUTO rotated to landscape"
|
||||
|
||||
# =====================================================================
|
||||
Write-Host "=== 4. V-C3 + V-C4: counter monotonic; >=45fps with animated desktop ==="
|
||||
# Static desktop is source-limited (~25fps): the honest window needs motion.
|
||||
$anim = Start-Job -ScriptBlock {
|
||||
Add-Type -TypeDefinition @"
|
||||
using System; using System.Runtime.InteropServices;
|
||||
public class An { [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y); }
|
||||
"@
|
||||
$end=(Get-Date).AddSeconds(14); $i=0
|
||||
while ((Get-Date) -lt $end) {
|
||||
$a=$i*0.18
|
||||
[An]::SetCursorPos([int](960+320*[Math]::Cos($a)), [int](540+320*[Math]::Sin($a))) | Out-Null
|
||||
Start-Sleep -Milliseconds 12; $i++
|
||||
}
|
||||
}
|
||||
Start-Sleep -Seconds 2 # let the encoder see motion
|
||||
$fA = FrameCountOrWake; $ta = Get-Date
|
||||
Start-Sleep -Milliseconds 2500
|
||||
$fB = FrameCountOrWake; $tb = Get-Date
|
||||
$fps = if ($fA -ge 0 -and $fB -gt $fA) { [Math]::Round(($fB-$fA)/(($tb-$ta).TotalSeconds),1) } else { -1 }
|
||||
Write-Host " frames $fA -> $fB fps=$fps (animated)"
|
||||
if ($fB -le $fA) { throw "V-C3 FAIL: counter stalled across rotation" }
|
||||
Write-Host " PASS: V-C3 session survived rotation"
|
||||
if ($fps -lt 45) { throw "V-C4 FAIL: $fps fps < 45 with animated desktop" }
|
||||
Write-Host " PASS: V-C4 fps >= 45"
|
||||
|
||||
# =====================================================================
|
||||
Write-Host "=== 5. V-C5: wake -> visible -> idle -> hidden ==="
|
||||
# R-8 discipline: "button absent" is NOT a valid judgement on its own - an
|
||||
# empty/broken dump would pass it trivially (measured 2026-11-01: after the
|
||||
# hide, the HUD TextView is GONE too, so the tree has zero text nodes).
|
||||
# Positive pair: keyboard button absent WHILE the video View node is present.
|
||||
#
|
||||
# Sequence is explicit (first run's implicit precondition raced: the fps
|
||||
# step's wake expired its 3s timer before we checked, so controls were
|
||||
# already hidden - correct product behavior, wrong harness assumption):
|
||||
# wake -> assert visible -> idle -> assert hidden.
|
||||
WakeControls
|
||||
$x5 = Get-Ui
|
||||
if (-not (ButtonByName $x5 $F_KB)) { throw "V-C5 FAIL: edge wake did not bring controls back" }
|
||||
Write-Host " visible after wake: OK"
|
||||
$deadline = (Get-Date).AddSeconds(6); $hidden = $false
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Milliseconds 600
|
||||
$x5 = Get-Ui
|
||||
$videoSeen = ($x5 -match 'class="android\.view\.View"')
|
||||
$kbGone = -not (ButtonByName $x5 $F_KB)
|
||||
if ($videoSeen -and $kbGone) { $hidden = $true; break }
|
||||
}
|
||||
if (-not $hidden) {
|
||||
throw "V-C5 FAIL: no (kb-gone AND video-present) state within 6s; last dump len=$($x5.Length)" }
|
||||
Write-Host " PASS: controls auto-hidden (video View still in tree - real hide, not empty dump)"
|
||||
|
||||
Write-Host "=== 5b. V-C2: video 16:9 and >=90% screen height (controls hidden) ==="
|
||||
$ui = Get-UiRetry
|
||||
$best = $null; $bestErr = 1e9
|
||||
foreach ($n in [regex]::Matches($ui,'<node[^>]*class="android\.view\.View"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')) {
|
||||
$x1=[int]$n.Groups[1].Value; $y1=[int]$n.Groups[2].Value
|
||||
$x2i=[int]$n.Groups[3].Value; $y2=[int]$n.Groups[4].Value
|
||||
$w=$x2i-$x1; $h=$y2-$y1
|
||||
if ($w -lt 300 -or $h -lt 100) { continue }
|
||||
$err = [Math]::Abs($w/[double]$h - 16.0/9.0)
|
||||
if ($err -lt $bestErr) { $bestErr=$err; $best=@{w=$w;h=$h} }
|
||||
}
|
||||
$d = LocalDisplay
|
||||
$hFrac = $best.h / [double]$d.h
|
||||
Write-Host (" video {0}x{1} ratio err {2:N4} height {3:N1}% of {4}x{5}" -f `
|
||||
$best.w,$best.h,$bestErr,($hFrac*100),$d.w,$d.h)
|
||||
if ($bestErr -gt 0.02) { throw "V-C2 FAIL: video not 16:9 (err $bestErr)" }
|
||||
if ($hFrac -lt 0.90) { throw ("V-C2 FAIL: video height {0:N1}% < 90%" -f ($hFrac*100)) }
|
||||
Write-Host " PASS: full-height 16:9 video (the tiny-video fix)"
|
||||
Stop-Job $anim -ErrorAction SilentlyContinue; Remove-Job $anim -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "=== 6. V-C5b: black-bar tap wakes controls WITHOUT touching the remote ==="
|
||||
Start-Sleep -Seconds 2 # animator fully stopped
|
||||
# The wake zone IS the letterbox black bar now (4a design change), so this
|
||||
# is the honest target. Also assert a video-interior tap is NOT consumed as
|
||||
# a wake - otherwise the black bar would have eaten real clicks too.
|
||||
$pc0 = [Cur4a]::Get()
|
||||
[void](AdbShell @("input","tap","100","$([int]($d.h/2))"))
|
||||
Start-Sleep -Milliseconds 600
|
||||
$pc1 = [Cur4a]::Get()
|
||||
$ui = Get-UiRetry
|
||||
$kb = ButtonByName $ui $F_KB
|
||||
$jump = [Math]::Max([Math]::Abs($pc1[0]-$pc0[0]), [Math]::Abs($pc1[1]-$pc0[1]))
|
||||
Write-Host (" black-bar tap: cursor jump=$jump px ; keyboard back: {0}" -f ($null -ne $kb))
|
||||
if (-not $kb) { throw "V-C5 FAIL: black-bar tap did not wake controls" }
|
||||
if ($jump -gt 1) { throw "V-C5b FAIL: black-bar tap leaked $jump px to remote" }
|
||||
Write-Host " PASS: wake works, zero remote leakage"
|
||||
|
||||
Write-Host "=== 6c. four buttons present; mode tap changes and restores ==="
|
||||
$kb2 = Find-VisibleButton $F_KB
|
||||
$mode = Find-VisibleButton $F_MODE $true
|
||||
$disc = Find-VisibleButton $F_DISC
|
||||
$orient = Find-VisibleButton $F_ORIENT $true
|
||||
if (-not ($kb2 -and $mode -and $disc -and $orient)) {
|
||||
throw ("V-C5 FAIL: missing buttons kb={0} mode={1} disc={2} orient={3}" -f `
|
||||
($null -ne $kb2),($null -ne $mode),($null -ne $disc),($null -ne $orient)) }
|
||||
$beforeLabel = Read-ButtonLabel $F_MODE $true
|
||||
if (-not $beforeLabel) { throw "V-C5 precondition FAIL: mode label unreadable" }
|
||||
[void](Tap-VisibleButton $F_MODE $true)
|
||||
Start-Sleep -Milliseconds 600
|
||||
$afterLabel = Read-ButtonLabel $F_MODE $true
|
||||
Write-Host " mode label: [$beforeLabel] -> [$afterLabel]"
|
||||
# null would trivially "change" from a real label - assert non-empty too (R-8)
|
||||
if (-not $afterLabel -or $beforeLabel -eq $afterLabel) { throw "V-C5 FAIL: mode tap changed nothing" }
|
||||
# converge back to relative (the product default the later proofs assume),
|
||||
# regardless of which way the flip above landed - tap until it reads relative.
|
||||
$F_MODE_REL = $F_MODE + -join ([char]0x76F8,[char]0x5BF9) # mode: relative
|
||||
$converged = $false
|
||||
for ($i=0; $i -lt 3 -and -not $converged; $i++) {
|
||||
$cur = Read-ButtonLabel $F_MODE $true
|
||||
if ($cur -eq $F_MODE_REL) { $converged = $true; break }
|
||||
[void](Tap-VisibleButton $F_MODE $true)
|
||||
Start-Sleep -Milliseconds 600
|
||||
}
|
||||
$restored = Read-ButtonLabel $F_MODE $true
|
||||
Write-Host " mode converged to relative: [$restored]"
|
||||
if (-not $converged -or $restored -ne $F_MODE_REL) {
|
||||
throw "V-C5 FAIL: mode not converged to relative (pollutes later proofs)" }
|
||||
Write-Host " PASS: buttons work, state restored"
|
||||
|
||||
# =====================================================================
|
||||
Write-Host "=== 7. V-C7: orientation cycle AUTO->L->P->SYS->AUTO ==="
|
||||
$seq = @($F_LAND, $F_PORT, $F_SYS, $F_AUTO)
|
||||
$wantLand = @($true, $false, $null, $true) # null = don't assert SYSTEM policy
|
||||
for ($i=0; $i -lt 4; $i++) {
|
||||
$orient = Find-VisibleButton $F_ORIENT $true
|
||||
if (-not $orient) { throw "V-C7 FAIL: orientation button unreachable after 5 wakes" }
|
||||
[void](Tap-VisibleButton $F_ORIENT $true)
|
||||
Start-Sleep -Milliseconds 1400
|
||||
$label = Read-ButtonLabel $F_ORIENT $true
|
||||
$d = LocalDisplay
|
||||
$land = $d.w -gt $d.h
|
||||
Write-Host (" step {0}: label=[{1}] want=[{2}] display={3}x{4} landscape={5}" -f ($i+1),$label,$seq[$i],$d.w,$d.h,$land)
|
||||
if ($label -ne $seq[$i]) { throw ("V-C7 FAIL: step {0} label [{1}] != [{2}]" -f ($i+1),$label,$seq[$i]) }
|
||||
if ($null -ne $wantLand[$i] -and $land -ne $wantLand[$i]) {
|
||||
throw ("V-C7 FAIL: step {0} landscape={1} want={2}" -f ($i+1),$land,$wantLand[$i]) }
|
||||
}
|
||||
Write-Host " PASS: full cycle with display following each policy"
|
||||
& powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-c7-final.png" | Out-Null
|
||||
}
|
||||
finally {
|
||||
# 4a pollution discipline: leave no live session behind - the next proof's
|
||||
# Restart-AppForm force-stops the app mid-session, which strands the agent's
|
||||
# serial accept in a zombie (N11) and the run after that fails to connect
|
||||
# (measured 2026-11-01: agent alive but no longer serving handshakes).
|
||||
[void](AdbShell @("am","force-stop","com.verify.remote"))
|
||||
Start-Sleep -Milliseconds 500
|
||||
Get-Job | Stop-Job -ErrorAction SilentlyContinue
|
||||
Get-Job | Remove-Job -Force -ErrorAction SilentlyContinue
|
||||
[void](AdbShell @("svc","power","stayon","false"))
|
||||
if ($origIme -and $origIme -ne "null") {
|
||||
[void](AdbShell @("ime","enable",$origIme))
|
||||
[void](AdbShell @("ime","set",$origIme))
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "=== LANDSCAPE PROOF COMPLETE ==="
|
||||
@ -23,7 +23,12 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repo = (Resolve-Path "$PSScriptRoot\..").Path
|
||||
$adb = "D:\working\android\sdk\platform-tools\adb.exe"
|
||||
$tmp = "$env:TEMP\remote-phone-proof"
|
||||
# UNIQUE per-run dir (measured 2026-11-01): with a fixed path, this script's
|
||||
# own startup Remove-Item deleted the stop.flag that LEFTOVER targets from a
|
||||
# previous failed run were watching - they could never retire, kept grabbing
|
||||
# foreground every 60ms, and wrote their textbox content into the shared
|
||||
# received.txt ("abc\nabc" = old run's abc+ENTER + this run's abc).
|
||||
$tmp = "$env:TEMP\remote-phone-proof-$PID"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
||||
$pidFile = "$tmp\target.pid"
|
||||
@ -94,12 +99,40 @@ function Get-PhoneUiText {
|
||||
return [System.IO.File]::ReadAllText("$tmp\ui.xml", [System.Text.Encoding]::UTF8)
|
||||
}
|
||||
|
||||
# 4a adaptation: in landscape the HUD TextView hides WITH the auto-hiding
|
||||
# control bar, so "RTT=" disappears while the session is perfectly alive
|
||||
# (measured 2026-11-01: this proof died at step 4 "session died after ENTER"
|
||||
# on a healthy 22fps stream). Detect the hidden state (video View node present,
|
||||
# zero Button nodes) and wake via the letterbox black bar - that tap is
|
||||
# consumed by the app and never reaches the remote.
|
||||
function Phone-DisplaySize {
|
||||
# NB: this script's Adb-Shell returns @{out;code}, not a bare string.
|
||||
$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
|
||||
}
|
||||
|
||||
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 }
|
||||
if (Wake-Controls-If-Hidden $x) {
|
||||
$x = Get-PhoneUiText
|
||||
if ($x -match "RTT=") { $outXml.Value = $x; return $true }
|
||||
}
|
||||
}
|
||||
$outXml.Value = (Get-PhoneUiText)
|
||||
return $false
|
||||
@ -249,8 +282,10 @@ try {
|
||||
$vals += [regex]::Match($v, 'text="([^"]*)"').Groups[1].Value
|
||||
}
|
||||
}
|
||||
# NOTE: an EMPTY EditText reports its hint as text (verified: text="被控端 IP"),
|
||||
# so "field text == typed value" is the assertion, not "field text non-empty".
|
||||
# NOTE: an EMPTY EditText reports its hint as text (verified on device), so
|
||||
# "field non-empty" is NOT a usable judgement - the assertion is always
|
||||
# "field text == typed value". (ASCII-only comments: PS 5.1 decodes
|
||||
# BOM-less .ps1 as GBK.)
|
||||
Write-Host " form now: $($vals -join ' | ')"
|
||||
if ($vals.Count -lt 3) { throw "expected 3 fields after typing, got $($vals.Count)" }
|
||||
if ($vals[0] -ne $PhoneIp) { throw "V4 FAIL: IP field is [$($vals[0])], expected [$PhoneIp]" }
|
||||
|
||||
@ -34,17 +34,25 @@ $F_LONGPRESS = -join ([char]0x957F,[char]0x6309,[char]0x53F3,[char]0x952E)
|
||||
$F_RCLICK_TAG = -join ([char]0x53F3,[char]0x952E,[char]0x3D) # "right-click="
|
||||
$F_CLICK = -join ([char]0x5355,[char]0x51FB) # "single click"
|
||||
|
||||
# Read the persistent right-click counter from the HUD ("右键=N(source)").
|
||||
# Read the persistent right-click counter from the HUD ("right-click=N(source)").
|
||||
# Returns -1 when absent. A counter is used instead of the one-line debug text
|
||||
# because ACTION_UP immediately overwrites the debug line after a long press
|
||||
# (measured 2026-10-31: screen showed "长按结束", the firing evidence was gone).
|
||||
# (measured 2026-10-31: screen showed the long-press-END label, the firing evidence was gone).
|
||||
function RightClickCount {
|
||||
$m = [regex]::Match((Get-UiText), 'text="([^"]*RTT=[^"]*)"')
|
||||
if (-not $m.Success) { return -1 }
|
||||
$hud = [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value)
|
||||
$c = [regex]::Match($hud, "$F_RCLICK_TAG(\d+)")
|
||||
if ($c.Success) { return [int]$c.Groups[1].Value }
|
||||
return 0
|
||||
for ($t=0; $t -lt 3; $t++) {
|
||||
$x = Get-UiText
|
||||
$m = [regex]::Match($x, 'text="([^"]*RTT=[^"]*)"')
|
||||
if ($m.Success) {
|
||||
$hud = [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value)
|
||||
$c = [regex]::Match($hud, "$F_RCLICK_TAG(\d+)")
|
||||
if ($c.Success) { return [int]$c.Groups[1].Value }
|
||||
return 0
|
||||
}
|
||||
# 4a: in landscape the HUD hides WITH the controls (GONE, not empty text)
|
||||
Wake-Controls $x
|
||||
Start-Sleep -Milliseconds 400
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function Adb {
|
||||
@ -110,6 +118,40 @@ function ButtonStartingWith([string]$xml, [string]$prefix) {
|
||||
return $null
|
||||
}
|
||||
|
||||
# 4a self-adaptation: in landscape the control bar auto-hides after 3s idle
|
||||
# (and the HUD TextView hides with it). The wake tap goes to the LEFT BLACK BAR
|
||||
# (x=100, outside the letterboxed video) - the app consumes black-bar touches
|
||||
# only in the hidden state, so this never reaches the remote. Bottom-center is
|
||||
# NOT usable: with the controls hidden the video is full-height, so the bottom
|
||||
# edge lies INSIDE the picture and would fire a real remote click.
|
||||
function PhoneDisplay {
|
||||
$d = (AdbShell @("dumpsys","window","displays"))
|
||||
$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([string]$xml) {
|
||||
if ($xml -match 'class="android\.widget\.Button"') { return } # visible already
|
||||
$pd = PhoneDisplay
|
||||
[void](AdbShell @("input","tap","100","$([int]($pd.h/2))"))
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
# HUD text with wake-retry (4a: in landscape the HUD TextView hides WITH the
|
||||
# control bar - a plain read returns "" once the 3s timer expired, and each
|
||||
# dump itself costs ~2s so reads routinely straddle the expiry).
|
||||
function Get-HudText {
|
||||
for ($t=0; $t -lt 3; $t++) {
|
||||
$x = Get-UiText
|
||||
$m = [regex]::Match($x, 'text="([^"]*RTT=[^"]*)"')
|
||||
if ($m.Success) { return [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value) }
|
||||
Wake-Controls $x
|
||||
Start-Sleep -Milliseconds 400
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
# Toggle the pointer mode and PROVE it flipped by re-reading the button label.
|
||||
# The bottom bar sits at y=2256..2324, right against the system navigation bar;
|
||||
# a single tap there is not reliable (measured 2026-10-31: the tap landed but the
|
||||
@ -132,7 +174,24 @@ function Set-Mode([string]$want) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $cur) { throw "mode button not found (no Button whose text contains '$F_MODE_TAG')" }
|
||||
if (-not $cur) {
|
||||
# 4a: controls may be auto-hidden (landscape). Wake once and retry
|
||||
# inside the same try budget instead of throwing on the first miss.
|
||||
Wake-Controls $xml
|
||||
$xml = Get-UiText
|
||||
foreach ($n in [regex]::Matches($xml,'<node[^>]*class="android\.widget\.Button"[^>]*>')) {
|
||||
$t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
|
||||
if ($t.Contains($F_MODE_TAG)) {
|
||||
$b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
if ($b.Success) {
|
||||
$x1=[int]$b.Groups[1].Value; $y1=[int]$b.Groups[2].Value
|
||||
$x2=[int]$b.Groups[3].Value; $y2=[int]$b.Groups[4].Value
|
||||
$cur = @{ label=$t; cx=[int](($x1+$x2)/2); cy=[int](($y1+$y2)/2) }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $cur) { throw "mode button not found (no Button whose text contains '$F_MODE_TAG', even after wake)" }
|
||||
}
|
||||
Write-Host " mode button label now: [$($cur.label)] (try $try)"
|
||||
if ($cur.label.Contains($wantTag)) { return $true }
|
||||
[void](AdbShell @("input","tap","$($cur.cx)","$($cur.cy)"))
|
||||
@ -331,6 +390,29 @@ try {
|
||||
Write-Host " surface bounds = [$vx1,$vy1][$vx2,$vy2] center=($vcx,$vcy)"
|
||||
& powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-b1-live.png" | Out-Null
|
||||
|
||||
# 4a pollution guard: the mode PERSISTS in prefs across runs, and a run that
|
||||
# threw between Set-Mode "absolute" and its restore left absolute installed
|
||||
# (measured 2026-11-01: step 2 ran in absolute mode, the DOWN absolute move
|
||||
# "teleported" 1441px - correct product behavior, wrong precondition).
|
||||
# Force relative, then settle back into the hidden steady state: while the
|
||||
# controls are hidden, gestures never re-show them, so the geometry stays
|
||||
# stable for steps 2/2b/2c.
|
||||
$hudNow = Get-HudText
|
||||
Write-Host " mode at start: $hudNow"
|
||||
if ($hudNow.Contains($F_ABSOLUTE)) { [void](Set-Mode "relative") }
|
||||
Start-Sleep -Milliseconds 3800 # let the 3s auto-hide re-engage
|
||||
$xh = Get-UiText
|
||||
if ($xh -match 'class="android\.widget\.Button"') { throw "precondition FAIL: controls did not re-hide after 3.8s" }
|
||||
$hsb = SurfaceBounds $xh
|
||||
# NB: PowerShell variable names are CASE-INSENSITIVE - a local `$sh` here
|
||||
# silently clobbers `$SH` (screen height) and every later [PC]::Set($SW/2,$SH/2)
|
||||
# dies with "Object[] has no op_Division" (measured 2026-11-01). Prefix locals
|
||||
# distinctly ($hsb = hidden-state bounds).
|
||||
if (-not $hsb) { throw "precondition FAIL: no video bounds in hidden state" }
|
||||
$vx1=$hsb[0]; $vy1=$hsb[1]; $vx2=$hsb[2]; $vy2=$hsb[3]
|
||||
$vcx=[int](($vx1+$vx2)/2); $vcy=[int](($vy1+$vy2)/2)
|
||||
Write-Host " hidden steady-state bounds = [$vx1,$vy1][$vx2,$vy2] center=($vcx,$vcy)"
|
||||
|
||||
# =================================================================
|
||||
Write-Host "=== 2. V-B1a: relative mode moves PC pointer (swipe right) ==="
|
||||
# Park the PC cursor at a known point so the delta is unambiguous.
|
||||
@ -373,40 +455,89 @@ try {
|
||||
# absolute move. Tap at the far LEFT of the view; the pointer must not jump
|
||||
# to the left edge of the desktop.
|
||||
#
|
||||
# HARNESS DISCIPLINE (measured 2026-10-31): a swipe's final relative events
|
||||
# can still be in flight when we re-park the cursor. The first attempt showed
|
||||
# phantom "jumps" of 15px and 298px that were really the PREVIOUS swipe's
|
||||
# queued deltas landing after our SetCursorPos. So: wait for the link to go
|
||||
# quiet, then require the baseline to be STABLE across two reads before
|
||||
# judging. Otherwise this test would report a product defect that isn't one.
|
||||
Start-Sleep -Milliseconds 1200
|
||||
[PC]::Set([int]($SW*0.8), [int]($SH/2))
|
||||
Start-Sleep -Milliseconds 500
|
||||
$stable = $false
|
||||
for ($i=0; $i -lt 6; $i++) {
|
||||
$r1 = [PC]::Get(); Start-Sleep -Milliseconds 350; $r2 = [PC]::Get()
|
||||
if ($r1[0] -eq $r2[0] -and $r1[1] -eq $r2[1]) { $stable = $true; break }
|
||||
Write-Host " cursor still settling: ($($r1[0]),$($r1[1])) -> ($($r2[0]),$($r2[1]))"
|
||||
# DISCRIMINATION (2026-11-01, landscape): a real absolute-leak has an exact
|
||||
# signature - the pointer lands at the tap's desktop mapping
|
||||
# (0.05*1920=96, 540) and STAYS. Residual swipe events instead keep the
|
||||
# cursor DRIFTING for seconds (observed: stable-looking 4-read windows
|
||||
# followed by -890/-1418px surges - bursts with >1.2s gaps between them).
|
||||
# So: settle (6 equal reads), tap, then sample 4x over 2s.
|
||||
# all samples == before -> PASS
|
||||
# samples drift -> harness residue: retry (max 3)
|
||||
# settled AT leak point -> product FAIL (no retry will change it)
|
||||
# Absolute-leak landing point for this tap: view fraction 0.05 maps to
|
||||
# desktop x = 0.05*(SW-1); y = view center row -> desktop mid row.
|
||||
$leakX = [int][Math]::Round(0.05 * ($SW-1))
|
||||
$leakY = [int][Math]::Round((($vcy-$vy1) / [double]($vy2-$vy1)) * ($SH-1))
|
||||
$verdict = "retry"
|
||||
for ($att=1; $att -le 3 -and $verdict -eq "retry"; $att++) {
|
||||
Start-Sleep -Milliseconds 2000
|
||||
[PC]::Set([int]($SW*0.8), [int]($SH/2))
|
||||
Start-Sleep -Milliseconds 1500
|
||||
$stable = $false
|
||||
for ($i=0; $i -lt 8; $i++) {
|
||||
$runs = @()
|
||||
for ($k=0; $k -lt 6; $k++) { $runs += ,([PC]::Get()); Start-Sleep -Milliseconds 250 }
|
||||
$eq = $true
|
||||
foreach ($r in $runs) { if ($r[0] -ne $runs[0][0] -or $r[1] -ne $runs[0][1]) { $eq = $false } }
|
||||
if ($eq) { $stable = $true; break }
|
||||
Write-Host (" att{0}: cursor still settling (last {1},{2}); re-parking" -f $att,$runs[-1][0],$runs[-1][1])
|
||||
[PC]::Set([int]($SW*0.8), [int]($SH/2))
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
if (-not $stable) { throw "cannot obtain a stable cursor baseline (residual events never drained)" }
|
||||
$beforeTap = [PC]::Get()
|
||||
$tapX = [int]($vx1 + ($vx2-$vx1)*0.05)
|
||||
[void](AdbShell @("input","tap","$tapX","$vcy"))
|
||||
$samples = @()
|
||||
for ($k=0; $k -lt 4; $k++) { Start-Sleep -Milliseconds 500; $samples += ,([PC]::Get()) }
|
||||
$dbgTap = ((VisibleTexts (Get-UiText)) -join " || ")
|
||||
$maxJump = 0
|
||||
foreach ($s in $samples) {
|
||||
$j = [Math]::Max([Math]::Abs($s[0]-$beforeTap[0]), [Math]::Abs($s[1]-$beforeTap[1]))
|
||||
if ($j -gt $maxJump) { $maxJump = $j }
|
||||
}
|
||||
$last = $samples[-1]
|
||||
$drift = $false
|
||||
for ($k=1; $k -lt 4; $k++) {
|
||||
if ($samples[$k][0] -ne $samples[0][0] -or $samples[$k][1] -ne $samples[0][1]) { $drift = $true }
|
||||
}
|
||||
$atLeak = ([Math]::Abs($last[0]-$leakX) -le 8) -and ([Math]::Abs($last[1]-$leakY) -le 8)
|
||||
# NB: "$_[0]" inside a double-quoted string expands only `$_` and prints
|
||||
# "[0]" literally (PS 5.1) - use explicit sub-expressions.
|
||||
$sampleStr = ($samples | ForEach-Object { "(" + $_[0] + "," + $_[1] + ")" }) -join " "
|
||||
Write-Host (" att{0}: tap ({1},{2}) -> samples {3} maxJump={4} drift={5} atLeakPoint={6} (leak=({7},{8}))" -f `
|
||||
$att,$beforeTap[0],$beforeTap[1],$sampleStr,$maxJump,$drift,$atLeak,$leakX,$leakY)
|
||||
if ($dbgTap.Contains($F_LONGPRESS)) { throw "tap was interpreted as a long press; harness timing is wrong" }
|
||||
if ($atLeak) { throw ("V-B1a FAIL: pointer settled at the absolute-leak point ({0},{1}) - DOWN sent an absolute move" -f $last[0],$last[1]) }
|
||||
if ($maxJump -le 8 -and -not $drift) { $verdict = "pass"; break }
|
||||
if (-not $drift) {
|
||||
# settled far away but NOT at the leak point: some other single event
|
||||
# moved it - that IS a product-relevant teleport, retry once more
|
||||
# before condemning (a queued burst can also settle far away).
|
||||
Write-Host " att${att}: settled far (jump=$maxJump) but not at leak point - treating as residue, retrying"
|
||||
}
|
||||
}
|
||||
if (-not $stable) { throw "cannot obtain a stable cursor baseline (residual relative events never drained)" }
|
||||
$beforeTap = [PC]::Get()
|
||||
$tapX = [int]($vx1 + ($vx2-$vx1)*0.05)
|
||||
[void](AdbShell @("input","tap","$tapX","$vcy"))
|
||||
Start-Sleep -Milliseconds 700
|
||||
$afterTap = [PC]::Get()
|
||||
$jump = [Math]::Max([Math]::Abs($afterTap[0] - $beforeTap[0]), [Math]::Abs($afterTap[1] - $beforeTap[1]))
|
||||
$dbgTap = ((VisibleTexts (Get-UiText)) -join " || ")
|
||||
Write-Host " tap at view-left x=$tapX ; pointer ($($beforeTap[0]),$($beforeTap[1])) -> ($($afterTap[0]),$($afterTap[1])) jump=$jump px"
|
||||
if ($dbgTap.Contains($F_LONGPRESS)) { throw "tap was interpreted as a long press; harness timing is wrong" }
|
||||
if ($jump -gt 8) { throw "V-B1a FAIL: touch-down teleported the pointer by $jump px (absolute move leaked into relative mode)" }
|
||||
if ($verdict -ne "pass") { throw "V-B1a FAIL: pointer moved on touch-down in relative mode across 3 attempts (residual or teleport - see samples above)" }
|
||||
Write-Host " PASS: relative mode leaves the pointer where it was (no teleport)"
|
||||
|
||||
# =================================================================
|
||||
Write-Host "=== 3. V-B2: switch to ABSOLUTE mode, re-run R7 touch mapping ==="
|
||||
[void](Set-Mode "absolute")
|
||||
$hud2 = ""
|
||||
$m2 = [regex]::Match((Get-UiText),'text="([^"]*RTT=[^"]*)"')
|
||||
if ($m2.Success) { $hud2 = [System.Net.WebUtility]::HtmlDecode($m2.Groups[1].Value) }
|
||||
# 4a geometry race (measured 2026-11-01): Set-Mode WOK the controls, so the
|
||||
# video shrank to 1216x684 - but each dump costs ~2s and the auto-hide timer
|
||||
# is 3s, so by tap time the controls had hidden again and the video was back
|
||||
# to full height. Tapping with the stale visible-state geometry produced
|
||||
# "errors" of 281-357px that were pure harness error (y landed at the old
|
||||
# center row 341, not 540). Wait for the hidden steady state and sample
|
||||
# THOSE bounds - in hidden state a video tap only re-arms the timer, it never
|
||||
# re-shows the controls, so the geometry is stable for the whole R7 loop.
|
||||
Start-Sleep -Milliseconds 3800
|
||||
$hud2 = Get-HudText
|
||||
$sb = SurfaceBounds (Get-UiText)
|
||||
if (-not $sb) { throw "V-B2 FAIL: no video bounds in hidden steady state" }
|
||||
$vx1=$sb[0];$vy1=$sb[1];$vx2=$sb[2];$vy2=$sb[3]
|
||||
$vcx=[int](($vx1+$vx2)/2); $vcy=[int](($vy1+$vy2)/2)
|
||||
Write-Host " hidden steady-state bounds = [$vx1,$vy1][$vx2,$vy2] center=($vcx,$vcy)"
|
||||
Write-Host " HUD: $hud2"
|
||||
if (-not $hud2.Contains($F_ABSOLUTE)) { throw "V-B2 FAIL: HUD still does not say absolute; got [$hud2]" }
|
||||
Write-Host " PASS: mode switched to absolute (button label and HUD agree)"
|
||||
@ -443,21 +574,48 @@ try {
|
||||
|
||||
# Use a desktop of our own: the proof's WinForms target window (R-11: never
|
||||
# inject into a window we do not own - it would modify user data).
|
||||
# NB: the 3b version launched this WITHOUT its Mandatory params, so the
|
||||
# child died instantly and the right-click landed at raw screen center
|
||||
# (possible user window - near-miss on R-11). Launch properly now and park
|
||||
# the pointer INSIDE our own window: topmost form at (80,80) 420x140.
|
||||
$target = Join-Path $repo "tools\text_target.ps1"
|
||||
if (-not (Test-Path $target)) { throw "missing $target" }
|
||||
$tp = Start-Process powershell -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File",$target `
|
||||
-PassThru -WindowStyle Minimized
|
||||
Start-Sleep -Seconds 3
|
||||
Write-Host " target window started (pid $($tp.Id))"
|
||||
# UNIQUE per-run dir (same orphan-stop-flag trap as phone_chain_proof,
|
||||
# fixed 2026-11-01): startup Remove-Item must not delete the retirement
|
||||
# channel of targets left by a previous failed run.
|
||||
$ttmp = Join-Path $env:TEMP "vbx4a-$PID"
|
||||
New-Item -ItemType Directory -Force -Path $ttmp | Out-Null
|
||||
$pidFile = "$ttmp\target.pid"; $stopFile = "$ttmp\stop.flag"; $resultFile = "$ttmp\received.txt"
|
||||
Remove-Item $pidFile,$stopFile,$resultFile -ErrorAction SilentlyContinue
|
||||
$tp = Start-Process powershell -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File",$target, `
|
||||
"-PidFile",$pidFile,"-ResultFile",$resultFile,"-StopFile",$stopFile -PassThru -WindowStyle Hidden
|
||||
$deadline = (Get-Date).AddSeconds(8)
|
||||
while (-not (Test-Path $pidFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 200 }
|
||||
if (-not (Test-Path $pidFile)) { throw "target window did not start (no pid file)" }
|
||||
Write-Host " target window started (pid $(Get-Content $pidFile))"
|
||||
|
||||
# Park the pointer over the middle of the target window's edit area by
|
||||
# absolute injection first (so we know where the long press lands).
|
||||
[PC]::Set([int]($SW/2), [int]($SH/2))
|
||||
# Long-press and drag run in the HIDDEN steady state: geometry is stable
|
||||
# there (video taps only re-arm the hide timer, they never re-show the
|
||||
# controls), and the counter read wakes the HUD afterwards.
|
||||
function Enter-HiddenSteady {
|
||||
Start-Sleep -Milliseconds 3800
|
||||
$x = Get-UiText
|
||||
if ($x -match 'class="android\.widget\.Button"') { throw "precondition FAIL: controls did not hide" }
|
||||
$s = SurfaceBounds $x
|
||||
if (-not $s) { throw "precondition FAIL: no hidden-state video bounds" }
|
||||
return $s
|
||||
}
|
||||
|
||||
$rcBefore = RightClickCount # may wake the controls; re-enter hidden after
|
||||
$hsb4 = Enter-HiddenSteady
|
||||
$hx1=$hsb4[0];$hy1=$hsb4[1];$hx2=$hsb4[2];$hy2=$hsb4[3]
|
||||
$hcx=[int](($hx1+$hx2)/2); $hcy=[int](($hy1+$hy2)/2)
|
||||
Write-Host " right-click counter before: $rcBefore ; hidden center=($hcx,$hcy)"
|
||||
# The long press right-clicks AT THE REMOTE POINTER, so park the pointer on
|
||||
# our own target window first - the menu then opens over a window we own.
|
||||
[PC]::Set(290, 150)
|
||||
Start-Sleep -Milliseconds 300
|
||||
$preLong = [PC]::Get()
|
||||
$rcBefore = RightClickCount
|
||||
Write-Host " right-click counter before: $rcBefore"
|
||||
[void](AdbShell @("input","swipe","$vcx","$vcy","$vcx","$vcy","900")) # press & hold ~900ms, no movement
|
||||
[void](AdbShell @("input","swipe","$hcx","$hcy","$hcx","$hcy","900")) # press & hold ~900ms, no movement
|
||||
Start-Sleep -Milliseconds 700
|
||||
$rcAfter = RightClickCount
|
||||
$debug = ((VisibleTexts (Get-UiText)) -join " || ")
|
||||
@ -472,7 +630,8 @@ try {
|
||||
|
||||
Write-Host "=== 4b. V-B3 negative: a DRAG must not fire long press ==="
|
||||
$rcBeforeDrag = RightClickCount
|
||||
[void](AdbShell @("input","swipe","$vcx","$vcy",$([int]($vcx+120)),$([int]($vcy+80)),900))
|
||||
Enter-HiddenSteady | Out-Null
|
||||
[void](AdbShell @("input","swipe","$hcx","$hcy",$([int]($hcx+120)),$([int]($hcy+80)),900))
|
||||
Start-Sleep -Milliseconds 700
|
||||
$rcAfterDrag = RightClickCount
|
||||
$debug2 = ((VisibleTexts (Get-UiText)) -join " || ")
|
||||
@ -487,10 +646,18 @@ try {
|
||||
$rcBeforeTwo = RightClickCount
|
||||
# adb cannot synthesize a two-finger tap (lessons R-6): this is a MANUAL item.
|
||||
Write-Host " (two-finger tap is not drivable via adb - recorded as manual, not asserted here)"
|
||||
|
||||
Stop-Process -Id $tp.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
finally {
|
||||
# 4a pollution discipline: end with no live session - the agent's 10s idle
|
||||
# reclaim then happens BETWEEN runs, not in the middle of the next run's
|
||||
# connect (measured 2026-11-01: a stranded zombie left the next proof unable
|
||||
# to reach a live session at all).
|
||||
[void](AdbShell @("am","force-stop","com.verify.remote"))
|
||||
# target window exits via its stop flag (text_target contract); kill is the
|
||||
# fallback. In finally so a throw never leaves a topmost window on the user's desktop.
|
||||
if ($stopFile) { [void](New-Item -ItemType File -Force -Path $stopFile -ErrorAction SilentlyContinue) }
|
||||
Start-Sleep -Milliseconds 400
|
||||
if ($tp -and -not $tp.HasExited) { Stop-Process -Id $tp.Id -Force -ErrorAction SilentlyContinue }
|
||||
[void][PC]::Set($origCursor[0], $origCursor[1])
|
||||
[void](AdbShell @("svc","power","stayon","false"))
|
||||
if ($origIme -and $origIme -ne "null") {
|
||||
|
||||
@ -24,7 +24,11 @@ $ErrorActionPreference = "Stop"
|
||||
# Repo root derived from this script's location (tools/ is one level under the
|
||||
# repo root) so the proof runs on any clone, not just the author's D: drive.
|
||||
$repo = (Resolve-Path "$PSScriptRoot\..").Path
|
||||
$tmp = "$env:TEMP\remote-text-proof"
|
||||
# UNIQUE per run: a fixed path lets the startup Remove-Item delete the stop.flag
|
||||
# that leftover targets from a previous FAILED run are watching, so they never
|
||||
# retire and keep writing into the shared received.txt
|
||||
# (same trap fixed in phone_chain_proof, measured 2026-11-01).
|
||||
$tmp = "$env:TEMP\remote-text-proof-$PID"
|
||||
$base = "D:\working\environment"
|
||||
|
||||
$env:RUSTUP_HOME = "$base\rust"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user