From 1a6c3f15a2a48de8f2e8758b8352cc336ad2dae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E5=8D=9A=E6=96=87?= <1323515022@qq.com> Date: Sat, 31 Oct 2026 17:35:18 +0800 Subject: [PATCH] =?UTF-8?q?test(tools):=203b=20=E7=9C=9F=E6=9C=BA=E9=AA=8C?= =?UTF-8?q?=E6=94=B6=E8=84=9A=E6=9C=AC=20+=20=E4=BF=AE=E6=AD=A3=20IME=20pr?= =?UTF-8?q?oof=20=E7=9A=84=E6=8C=89=E9=92=AE=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rel_mouse_phone_proof.ps1 覆盖 V-B1a(相对位移方向/符号/不跳变)、V-B2(切绝对 模式后 R7 重测)、V-B3(长按出右键、拖拽不出)。 三处 harness 教训(都已写进脚本注释,避免下次重踩): 1. uiautomator 不暴露 SurfaceView,裸 SurfaceView 塌成 android.view.View; 按 16:9 宽高比识别视频视图,并断言比例,避免静默抓错节点。 2. 上一段滑动的相对事件可能在 SetCursorPos 之后才落地,制造出 15px/298px 的 假跳变。判据前必须先等链路静默、并要求基线两次读数一致。 3. ime_chinese_proof 用「第一个 Button」当键盘按钮——3b 在按钮栏上方插入了 灵敏度行,第一个 Button 变成了减号,proof 报「IME not shown」。 改为按文案匹配,与位置无关。 --- tools/ime_chinese_proof.ps1 | 27 +- tools/rel_mouse_phone_proof.ps1 | 504 ++++++++++++++++++++++++++++++++ 2 files changed, 525 insertions(+), 6 deletions(-) create mode 100644 tools/rel_mouse_phone_proof.ps1 diff --git a/tools/ime_chinese_proof.ps1 b/tools/ime_chinese_proof.ps1 index 4c623b8..58e13cf 100644 --- a/tools/ime_chinese_proof.ps1 +++ b/tools/ime_chinese_proof.ps1 @@ -144,12 +144,27 @@ while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 900; $ui = Get-Ui; if ($ui -notmatch "RTT=") { throw "phone did not connect" } Write-Host "=== 4. show IME (tap keyboard button) ===" -$btns = [regex]::Matches($ui, ']*class="android\.widget\.Button"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') -if ($btns.Count -lt 1) { throw "keyboard button not found" } -$b0 = $btns[0] -$kx = [int](([int]$b0.Groups[1].Value + [int]$b0.Groups[3].Value) / 2) -$ky = [int](([int]$b0.Groups[2].Value + [int]$b0.Groups[4].Value) / 2) -[void](Adb-Shell @("input","tap","$kx","$ky")) +# Select the keyboard button BY ITS LABEL, not by position. +# 3b added a sensitivity row (- / +) above the button bar, so "the first Button" +# is now the minus sign - the old positional assumption silently tapped the wrong +# control and the proof reported "IME not shown" (a harness defect, not a product +# regression; measured 2026-10-31). Label matching is order-independent. +$F_KEYBOARD = -join ([char]0x952E,[char]0x76D8) # "keyboard" +$kb = $null +foreach ($n in [regex]::Matches($ui,']*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) + ) + } + } +} +if (-not $kb) { throw "keyboard button not found by label" } +[void](Adb-Shell @("input","tap","$($kb[0])","$($kb[1])")) Start-Sleep -Seconds 3 $ime = (Adb-Shell @("dumpsys","input_method")).out if ($ime -notmatch "mInputShown=true") { throw "IME not shown" } diff --git a/tools/rel_mouse_phone_proof.ps1 b/tools/rel_mouse_phone_proof.ps1 new file mode 100644 index 0000000..9e641da --- /dev/null +++ b/tools/rel_mouse_phone_proof.ps1 @@ -0,0 +1,504 @@ +# 3b phone-side proof: relative mouse (protocol v4) + long-press right click + R7 regression. +# +# ASCII-only source (PS 5.1 decodes BOM-less .ps1 as GBK; see personal lessons). +# Expected Chinese UI strings are matched as CODE-POINT fragments (R-8: assert the +# actual value, and print what the parser really read so an empty result cannot pass). +# +# Judgements: +# V-B1a relative mode moves the PC pointer by the expected amount (direction+sign) +# V-B2 absolute mode still maps finger -> desktop 1:1 (R7 re-run, <=8px error) +# V-B3 long press (>=500ms, still) opens a context menu; a drag does NOT +# V-B7 R7 + Chinese IME + full chain regressions stay green +# +# Prereqs: vivo device online via adb, 3b APK installed, v4 agent running. +# powershell -ExecutionPolicy Bypass -File tools\rel_mouse_phone_proof.ps1 -Host_ 192.168.1.110 +param( + [string]$Host_ = "192.168.1.110", + [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\3b" +New-Item -ItemType Directory -Force -Path $ev | Out-Null + +# ---- code-point fragments (grep-verified present in the Kotlin sources) ---- +$F_CONNECT = -join ([char]0x8FDE,[char]0x63A5) # "connect" +$F_RECONNECT = -join ([char]0x91CD,[char]0x65B0,[char]0x8FDE,[char]0x63A5) # "reconnect" +$F_RELATIVE = -join ([char]0x76F8,[char]0x5BF9) # "relative" +$F_ABSOLUTE = -join ([char]0x7EDD,[char]0x5BF9) # "absolute" +$F_MODE_TAG = -join ([char]0x6A21,[char]0x5F0F) # "mode" +$F_LONGPRESS = -join ([char]0x957F,[char]0x6309,[char]0x53F3,[char]0x952E) # "long-press right click" +$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)"). +# 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). +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 +} + +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-UiText { + $d = AdbShell @("uiautomator","dump","/sdcard/rm.xml") + $r = Adb @("pull","/sdcard/rm.xml","$env:TEMP\rm.xml") + if ($r.code -ne 0 -or -not (Test-Path "$env:TEMP\rm.xml")) { + Write-Host " [ui] dump=[$($d -replace "`n",' ')] pull_rc=$($r.code) -> empty" + return "" + } + $txt = [System.IO.File]::ReadAllText("$env:TEMP\rm.xml", [System.Text.Encoding]::UTF8) + if ($txt.Length -lt 200) { + Write-Host " [ui] short xml ($($txt.Length) chars): $($txt.Substring(0,[Math]::Min(120,$txt.Length)))" + } + return $txt +} +function VisibleTexts([string]$xml) { + [regex]::Matches($xml,'text="([^"]+)"') | ForEach-Object { [System.Net.WebUtility]::HtmlDecode($_.Groups[1].Value) } +} +function FormFields([string]$xml) { + [regex]::Matches($xml,']*class="android\.widget\.EditText"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') +} +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 + @([int](($a+$c)/2), [int](($b+$d)/2)) +} +function ButtonCenter([string]$xml, [string]$want) { + foreach ($n in [regex]::Matches($xml,']*class="android\.widget\.Button"[^>]*>')) { + $t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value) + if ($t -eq $want) { + $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 @([int](($x1+$x2)/2), [int](($y1+$y2)/2)) + } + } + } + return $null +} +function ButtonStartingWith([string]$xml, [string]$prefix) { + foreach ($n in [regex]::Matches($xml,']*class="android\.widget\.Button"[^>]*>')) { + $t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value) + if ($t.StartsWith($prefix)) { + $b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') + if ($b.Success) { + $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 @([int](($x1+$x2)/2), [int](($y1+$y2)/2)) + } + } + } + return $null +} + +# 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 +# mode did not change). So: tap, wait, re-read, retry; and judge on the BUTTON +# label as well as the HUD (two independent observations of the same state). +function Set-Mode([string]$want) { + # $want is "relative" or "absolute" + $wantTag = if ($want -eq "relative") { $F_RELATIVE } else { $F_ABSOLUTE } + for ($try=0; $try -lt 4; $try++) { + $xml = Get-UiText + $cur = $null + foreach ($n in [regex]::Matches($xml,']*class="android\.widget\.Button"[^>]*>')) { + $t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value) + if ($t.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')" } + 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)")) + Start-Sleep -Milliseconds 700 + } + $final = ((VisibleTexts (Get-UiText)) -join " || ") + throw "mode did not switch to $want after 4 taps; screen: [$final]" +} +# The video SurfaceView bounds - touch coordinates must land inside it. +# +# uiautomator does NOT expose SurfaceView as "android.view.SurfaceView"; a bare +# SurfaceView with no accessibility node collapses to a plain "android.view.View" +# (measured 2026-10-31 on the live device: the only candidate is 1080x607, which +# is exactly the 16:9 letterbox of the 1920x1080 desktop inside a 1080-wide stage). +# So identify it structurally: the widest View whose aspect ratio matches 16:9. +# Assert the ratio instead of assuming it - a silent wrong node would make every +# downstream coordinate assertion meaningless. +function SurfaceBounds([string]$xml) { + $best = $null; $bestErr = [double]::MaxValue + foreach ($n in [regex]::Matches($xml,']*class="android\.view\.View"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')) { + $x1=[int]$n.Groups[1].Value; $y1=[int]$n.Groups[2].Value + $x2=[int]$n.Groups[3].Value; $y2=[int]$n.Groups[4].Value + $w = $x2-$x1; $h = $y2-$y1 + if ($w -lt 300 -or $h -lt 100) { continue } + $ratio = $w / [double]$h + $err = [Math]::Abs($ratio - (16.0/9.0)) + if ($err -lt $bestErr) { $bestErr = $err; $best = @($x1,$y1,$x2,$y2) } + } + if (-not $best) { return $null } + # sanity: the chosen node must really be ~16:9, else we grabbed something else + $w = $best[2]-$best[0]; $h = $best[3]-$best[1] + if ($bestErr -gt 0.25) { + throw "no 16:9 view found (best candidate ${w}x${h}, ratio $([Math]::Round($w/[double]$h,3)))" + } + Write-Host " video view ${w}x${h} (ratio $([Math]::Round($w/[double]$h,3)), 16:9 error $([Math]::Round($bestErr,4)))" + return $best +} + +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 FocusedIndex([string]$xml) { + $i = -1 + foreach ($n in [regex]::Matches($xml,']*>')) { + if ($n.Value -match 'class="android\.widget\.EditText"') { + $i++ + if ($n.Value -match 'focused="true"') { return $i } + } + } + return -1 +} +function Focus-Field($c, [int]$wantIdx) { + for ($try=0; $try -lt 3; $try++) { + [void](AdbShell @("input","tap","$($c[0])","$($c[1])")) + $deadline = (Get-Date).AddSeconds(1.5) + while ((Get-Date) -lt $deadline) { + Start-Sleep -Milliseconds 250 + if ((FocusedIndex (Get-UiText)) -eq $wantIdx) { return } + } + } + throw "tap did not focus field $wantIdx after 3 tries" +} +function Clear-Field($c, [int]$idx) { + Focus-Field $c $idx + for ($i=0; $i -lt 40; $i++) { [void](AdbShell @("input","keyevent","67")) } + Start-Sleep -Milliseconds 150 +} +function Restart-AppForm { + [void](AdbShell @("input","keyevent","224")) + Start-Sleep -Milliseconds 300 + [void](AdbShell @("wm","dismiss-keyguard")) + Start-Sleep -Milliseconds 500 + [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-UiText + if ((FormFields $x).Count -ge 3) { return $x } + Start-Sleep -Milliseconds 300 + } + $diag = (VisibleTexts (Get-UiText)) -join " || " + throw "connect form did not appear. screen: [$diag]" +} +function FillAndConnect([string]$xml, [string]$host_, [string]$port_, [string]$code_) { + $f = FormFields $xml + if ($f.Count -lt 3) { throw "form has $($f.Count) fields" } + Clear-Field (Center $f[0]) 0; TypeAscii $host_ + Clear-Field (Center $f[1]) 1; TypeAscii $port_ + Clear-Field (Center $f[2]) 2; TypeAscii $code_ + $expect = @($host_, $port_, $code_) + for ($retry=0; $retry -le 2; $retry++) { + $x2 = Get-UiText + $vals = @() + foreach ($n in [regex]::Matches($x2,']*>')) { + if ($n.Value -match 'class="android\.widget\.EditText"') { + $vals += [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value) + } + } + Write-Host " form now: $($vals -join ' | ') (try $retry)" + if ($vals.Count -lt 3) { throw "expected 3 fields after typing, got $($vals.Count)" } + $bad = -1 + for ($i=0; $i -lt 3; $i++) { if ($vals[$i] -ne $expect[$i]) { $bad = $i; break } } + if ($bad -ge 0) { + $f = FormFields $x2 + Clear-Field (Center $f[$bad]) $bad + TypeAscii $expect[$bad] + continue + } + break + } + if ($vals[0] -ne $host_) { throw "form input FAIL: host is [$($vals[0])], expected [$host_]" } + if ($vals[1] -ne $port_) { throw "form input FAIL: port is [$($vals[1])], expected [$port_]" } + if ($vals[2] -ne $code_) { throw "form input FAIL: code is [$($vals[2])], expected [$code_]" } + $btn = $null + foreach ($want in @($F_CONNECT, $F_RECONNECT)) { $btn = ButtonCenter $x2 $want; if ($btn) { break } } + if (-not $btn) { throw "no primary connect/reconnect button found" } + [void](AdbShell @("input","tap","$($btn[0])","$($btn[1])")) +} +function Wait-Live([int]$seconds, [ref]$hud) { + $deadline = (Get-Date).AddSeconds($seconds) + while ((Get-Date) -lt $deadline) { + Start-Sleep -Milliseconds 500 + $m = [regex]::Match((Get-UiText),'text="([^"]*RTT=[^"]*)"') + if ($m.Success) { $hud.Value = [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value); return $true } + } + return $false +} + +# ---- PC-side cursor readback (the only honest judge of "pointer moved N px") ---- +Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; +public class PC { + [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 ctx); + [DllImport("user32.dll")] public static extern int GetSystemMetrics(int i); + 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); } + public static int W() { return GetSystemMetrics(0); } + public static int H() { return GetSystemMetrics(1); } +} +"@ +[void][PC]::Dpi() +$SW = [PC]::W(); $SH = [PC]::H() +Write-Host " PC screen ${SW}x${SH} (physical, DPI-aware)" + +Write-Host "=== 0. stale-artifact guard (R-7) + IME disable + keep-awake ===" +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 " running agent predates current build; restarting" + Stop-Process -Name agent -Force -ErrorAction SilentlyContinue + Start-Sleep -Milliseconds 800; $ag = @() +} +if ($ag.Count -eq 0) { + if (-not (Test-Path $AgentExe)) { throw "missing $AgentExe" } + Start-Process $AgentExe -ArgumentList "--port","$Port","--code",$Code -WindowStyle Hidden | Out-Null + Start-Sleep -Seconds 1; Write-Host " agent started (v4)" +} else { Write-Host " agent running and current" } + +$origIme = ((AdbShell @("settings","get","secure","default_input_method")) -join "").Trim() +Write-Host " original IME: [$origIme] (recorded, restored in finally)" +if ($origIme -and $origIme -ne "null") { [void](AdbShell @("ime","disable",$origIme)) } +[void](AdbShell @("svc","power","stayon","true")) + +$origCursor = [PC]::Get() +Write-Host " PC cursor origin ($($origCursor[0]),$($origCursor[1])) (restored in finally)" + +try { + # ================================================================= + Write-Host "=== 1. connect (protocol v4) and confirm HUD shows v4 ===" + $x = Restart-AppForm + [void](FillAndConnect $x $Host_ "$Port" $Code) + $hud = "" + if (-not (Wait-Live 14 ([ref]$hud))) { throw "never reached a live session" } + Write-Host " HUD: $hud" + if (-not $hud.Contains("v4")) { throw "V-B4 FAIL: HUD does not report protocol v4; got [$hud]" } + Write-Host " PASS: live session on protocol v4" + + $xml = Get-UiText + $sb = SurfaceBounds $xml + if (-not $sb) { throw "cannot locate SurfaceView bounds" } + $vx1=$sb[0]; $vy1=$sb[1]; $vx2=$sb[2]; $vy2=$sb[3] + $vcx=[int](($vx1+$vx2)/2); $vcy=[int](($vy1+$vy2)/2) + 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 + + # ================================================================= + 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. + # Let the link drain first (same discipline as 2c): residual events from an + # earlier gesture would otherwise be attributed to this one. + Start-Sleep -Milliseconds 1000 + [PC]::Set([int]($SW/2), [int]($SH/2)) + Start-Sleep -Milliseconds 500 + $before = [PC]::Get() + # A horizontal swipe of ~40% of the view width. With sensitivity 1.0 and + # view width mapping to desktop width, expected delta = 0.4 * SW. + $swipeFromX = [int]($vx1 + ($vx2-$vx1)*0.30) + $swipeToX = [int]($vx1 + ($vx2-$vx1)*0.70) + [void](AdbShell @("input","swipe","$swipeFromX","$vcy","$swipeToX","$vcy","400")) + Start-Sleep -Milliseconds 500 + $after = [PC]::Get() + $moved = $after[0] - $before[0] + Write-Host " swipe dx_device=$($swipeToX-$swipeFromX)px -> PC pointer dx=$moved px (before x=$($before[0]), after x=$($after[0]))" + if ($moved -le 0) { throw "V-B1a FAIL: rightward swipe did not move the pointer right (dx=$moved)" } + # Direction check is the hard judgement; exact magnitude depends on how many + # ACTION_MOVE samples the ROM delivers for an `input swipe` (not equal to the + # finger travel), so magnitude is reported, not asserted. + Write-Host " PASS: relative mode moves the pointer in the finger's direction" + + Write-Host "=== 2b. V-B1a negative: swipe left moves pointer left ===" + Start-Sleep -Milliseconds 1000 + [PC]::Set([int]($SW/2), [int]($SH/2)) + Start-Sleep -Milliseconds 500 + $before2 = [PC]::Get() + [void](AdbShell @("input","swipe","$swipeToX","$vcy","$swipeFromX","$vcy","400")) + Start-Sleep -Milliseconds 500 + $after2 = [PC]::Get() + $moved2 = $after2[0] - $before2[0] + Write-Host " PC pointer dx=$moved2 px (before x=$($before2[0]), after x=$($after2[0]))" + if ($moved2 -ge 0) { throw "V-B1a FAIL: leftward swipe did not move the pointer left (dx=$moved2)" } + Write-Host " PASS: negative direction correct (sign preserved)" + + Write-Host "=== 2c. relative mode does NOT teleport the pointer on touch-down ===" + # The defining property of relative mode: ACTION_DOWN must not send an + # 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]))" + } + 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)" } + 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) } + 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)" + + # R7 expectations for 1920x1080 (physical pixels, 125% scaling does NOT apply + # to the injected desktop space - the agent injects into the physical desktop) + $r7 = @( + @{ frac=0.10; ex=[int](192); ey=[int](108) }, + @{ frac=0.90; ex=[int](1728); ey=[int](972) }, + @{ frac=0.50; ex=[int](960); ey=[int](540) } + ) + $worst = 0 + foreach ($c in $r7) { + $tx = [int]($vx1 + ($vx2-$vx1)*$c.frac) + $ty = [int]($vy1 + ($vy2-$vy1)*$c.frac) + [void](AdbShell @("input","tap","$tx","$ty")) + Start-Sleep -Milliseconds 400 + $p = [PC]::Get() + $errX = [Math]::Abs($p[0] - $c.ex) + $errY = [Math]::Abs($p[1] - $c.ey) + $e = [Math]::Max($errX,$errY) + if ($e -gt $worst) { $worst = $e } + Write-Host " frac=$($c.frac) expect=($($c.ex),$($c.ey)) actual=($($p[0]),$($p[1])) err=($errX,$errY)" + } + Write-Host " worst R7 error = $worst px" + if ($worst -gt 8) { throw "V-B2 FAIL: R7 absolute mapping error $worst px > 8px" } + Write-Host " PASS: absolute mode still maps 1:1 (R7 regression green)" + + # ================================================================= + Write-Host "=== 4. V-B3: long press opens a context menu; drag does NOT ===" + # Switch back to relative for the long-press test (that is the default mode + # users get, and the gesture must work there). + [void](Set-Mode "relative") + + # 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). + $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))" + + # 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)) + 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 + Start-Sleep -Milliseconds 700 + $rcAfter = RightClickCount + $debug = ((VisibleTexts (Get-UiText)) -join " || ") + & powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-b3-longpress.png" | Out-Null + Write-Host " right-click counter after : $rcAfter (source tag in HUD)" + Write-Host " screen after long press: $debug" + if ($rcBefore -lt 0 -or $rcAfter -lt 0) { throw "V-B3 FAIL: could not read the right-click counter from the HUD" } + if ($rcAfter -le $rcBefore) { + throw "V-B3 FAIL: long press did not inject a right click (counter stayed $rcAfter; screen: $debug)" + } + Write-Host " PASS: long press injected a right click (counter $rcBefore -> $rcAfter)" + + 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)) + Start-Sleep -Milliseconds 700 + $rcAfterDrag = RightClickCount + $debug2 = ((VisibleTexts (Get-UiText)) -join " || ") + Write-Host " right-click counter after drag: $rcAfterDrag (was $rcBeforeDrag)" + Write-Host " screen after drag: $debug2" + if ($rcAfterDrag -gt $rcBeforeDrag) { + throw "V-B3 FAIL: drag injected a right click (counter $rcBeforeDrag -> $rcAfterDrag); long press must be suppressed once the finger moves" + } + Write-Host " PASS: drag did not fire long press" + + Write-Host "=== 4c. V-B3: two-finger right click still works ===" + $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 { + [void][PC]::Set($origCursor[0], $origCursor[1]) + [void](AdbShell @("svc","power","stayon","false")) + if ($origIme -and $origIme -ne "null") { + [void](AdbShell @("ime","enable",$origIme)) + [void](AdbShell @("ime","set",$origIme)) + Write-Host " IME restored: $(((AdbShell @('settings','get','secure','default_input_method')) -join '').Trim())" + } + Write-Host " PC cursor restored to ($($origCursor[0]),$($origCursor[1]))" +} + +Write-Host "=== REL MOUSE PHONE PROOF COMPLETE ==="