fix(android): BACK 键改用自测键盘可见性——isAcceptingText 会让会话永远断不掉

真机实测(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,所以判据改为比对实际输入值。
This commit is contained in:
郑博文 2026-10-30 13:31:28 +08:00
parent 9792662181
commit 8993a38658
2 changed files with 60 additions and 6 deletions

View File

@ -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) {

View File

@ -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, '<node[^>]*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, '<node[^>]*>')) {
$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, '<node[^>]*class="android\.widget\.Button"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
if (-not $btn.Success) { throw "connect button not found" }
$bc = Center $btn