From 8993a386584c159d9578143418c3db53c15edcea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E5=8D=9A=E6=96=87?= <1323515022@qq.com> Date: Fri, 30 Oct 2026 13:31:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(android):=20BACK=20=E9=94=AE=E6=94=B9?= =?UTF-8?q?=E7=94=A8=E8=87=AA=E6=B5=8B=E9=94=AE=E7=9B=98=E5=8F=AF=E8=A7=81?= =?UTF-8?q?=E6=80=A7=E2=80=94=E2=80=94isAcceptingText=20=E4=BC=9A=E8=AE=A9?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E6=B0=B8=E8=BF=9C=E6=96=AD=E4=B8=8D=E6=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真机实测(vivo S6 + 讯飞,2026-10-30):软键盘已隐藏、mInputShown=false, 但 imm.isAcceptingText 仍返回 true——只要 InputConnection 还绑着它就是 true。 原先用它判断「先关键盘还是先断会话」,后果是第一次 BACK 空转,第二次也进不来 (我们已 return true),会话再也断不掉。这不是推测,是完整链路 proof 跑到 step 5 时实测失败的。 改为自测:比较根窗口可见区域与窗口高度,被 IME 吃掉超过 15% 视为可见; 用 OnGlobalLayoutListener 被动跟踪(没有公开 API 能直接查询,API 34 上 getInputMethodWindowVisibleHeight 也不是公开接口)。阈值用比例不用固定像素。 验证:diag 依次显示「键盘可见」→「键盘隐藏」→ 会话断开回表单; 随后完整链路 proof 与中文 proof 均复跑通过。 同时修正 phone_chain_proof 的 V4 断言:uiautomator 属性顺序是 text= 在 class= 之前,原正则要求反序,导致打印空表单行仍判 PASS(假绿)。 空 EditText 的 text 会回显 hint,所以判据改为比对实际输入值。 --- .../kotlin/com/verify/remote/MainActivity.kt | 50 +++++++++++++++++-- tools/phone_chain_proof.ps1 | 16 +++++- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/apps/android-viewer/app/src/main/kotlin/com/verify/remote/MainActivity.kt b/apps/android-viewer/app/src/main/kotlin/com/verify/remote/MainActivity.kt index 3766174..be060ca 100644 --- a/apps/android-viewer/app/src/main/kotlin/com/verify/remote/MainActivity.kt +++ b/apps/android-viewer/app/src/main/kotlin/com/verify/remote/MainActivity.kt @@ -94,6 +94,10 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener { root.addView(remotePanel, LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)) setContentView(root) + // 键盘可见性只能靠窗口布局变化被动感知(没有公开 API 能直接查询)。 + // 每次根视图布局完成都重算一次,覆盖「用户点键盘按钮」「IME 自己收起」两条路径。 + root.viewTreeObserver.addOnGlobalLayoutListener { updateKeyboardVisibility() } + showConnectState("未连接") // intent 入口优先级高于表单默认值:build-android.ps1 与调试脚本依赖它 @@ -275,9 +279,44 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener { override fun isReadyForInput(): Boolean = handle != 0L + /** + * 软键盘是否可见(自测,不依赖 InputMethodManager 的间接信号)。 + * + * 为什么必须自测:真机实测(vivo S6 + 讯飞,2026-10-30)键盘隐藏后 + * `imm.isAcceptingText` **仍返回 true**——只要 InputConnection 还绑着它就一直 true。 + * 用它判断"先关键盘还是先断会话",会让第一次 BACK 空转、第二次也进不来 + * (我们已 return true),会话再也断不掉。这是真机暴露的缺陷,不是推测。 + * + * 做法:比较根窗口可见区域高度与窗口高度。键盘占位时可见区域显著变矮。 + * 阈值用 15% 而不是固定像素,避免不同屏幕密度下误判。 + */ + private var keyboardVisible = false + + private val visibleFrame = android.graphics.Rect() + + private fun updateKeyboardVisibility() { + val decor = window.decorView + decor.getWindowVisibleDisplayFrame(visibleFrame) + val screenH = decor.height.takeIf { it > 0 } ?: return + val visibleH = visibleFrame.height() + // 底部被 IME 吃掉的高度超过屏幕 15% → 认为键盘可见 + val hiddenByIme = screenH - visibleH + val nowVisible = hiddenByIme > screenH * 0.15 + if (nowVisible != keyboardVisible) { + keyboardVisible = nowVisible + lastTouchDbg = if (nowVisible) "键盘可见" else "键盘隐藏" + refreshStatus() + } + } + + override fun onConfigurationChanged(newConfig: android.content.res.Configuration) { + super.onConfigurationChanged(newConfig) + updateKeyboardVisibility() + } + /** * 按键转发(物理键盘 / adb 注入 / 无 IME 时的软键盘): - * - 返回键:先收 IME,否则断开回表单(阶段 2a 语义延续); + * - 返回键:**键盘可见时**先收键盘,否则断开回表单(阶段 2a 语义延续); * - **可打印**的 `unicodeChar`(0x20 起、排除 0x7F)→ v3 **Text** 通道: * 字符注入与远端键盘布局无关。注意 `getUnicodeChar()` 给的是单个 * UTF-16 code unit 而非码点,所以这条路只覆盖 BMP;增补平面字符由 @@ -295,10 +334,13 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener { */ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { if (keyCode == KeyEvent.KEYCODE_BACK) { - val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - // isAcceptingText = 当前有 IME 连接且正在接收输入(API 17+) - if (imm.isAcceptingText) { + // 用自测的键盘可见性,不用 imm.isAcceptingText(见 updateKeyboardVisibility + // 注释:真机上键盘隐藏后它仍为 true,会让会话永远断不掉) + updateKeyboardVisibility() + if (keyboardVisible) { + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(surfaceView.windowToken, 0) + keyboardVisible = false return true } if (handle != 0L) { diff --git a/tools/phone_chain_proof.ps1 b/tools/phone_chain_proof.ps1 index db3dfea..4fd3914 100644 --- a/tools/phone_chain_proof.ps1 +++ b/tools/phone_chain_proof.ps1 @@ -230,9 +230,21 @@ try { 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, ']*class="android\.widget\.EditText"[^>]*text="([^"]*)"') | - ForEach-Object { $_.Groups[1].Value } + # Attribute order in uiautomator dumps is text= ... class= ... bounds=; + # matching "class=...text=" silently finds nothing (seen 2026-10-30: printed + # an empty form line). Parse per-node and pull attributes independently. + $vals = @() + foreach ($n in [regex]::Matches($xml3, ']*>')) { + $v = $n.Value + if ($v -match 'class="android\.widget\.EditText"') { + $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". Write-Host " form now: $($vals -join ' | ')" + if ($vals.Count -lt 3) { throw "expected 3 fields after typing, got $($vals.Count)" } + if ($vals[0] -ne "192.168.1.180") { throw "V4 FAIL: IP field is [$($vals[0])], typing did not land" } $btn = [regex]::Match($xml3, ']*class="android\.widget\.Button"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') if (-not $btn.Success) { throw "connect button not found" } $bc = Center $btn