Android焦点窗口变化导致遥控键值监听失效问题分析
最近在做语音全局控制Android系统功能,通过集成第三方语音识别sdk得到相关控制指令,然后将指令通过进程间通信传递给当前应用并作出响应。
有很多通用指令,比如播放/暂停,Android系统本身就有全局控制指令:KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,只需模拟发送该遥控键值即可,如下:
Instrumentation inst = new Instrumentation();// 调用inst对象的按键模拟方法inst.sendKeyDownUpSync(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
其他控制指令也可以类似处理,模拟发送一个遥控键值,然后在目标应用中通过监听键值事件进行功能处理即可:
@Overridepublic boolean onKeyDown(int keyCode, KeyEvent event) {switch (keyCode){case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:break;}return super.onKeyDown(keyCode, event);}
正常这个逻辑是没问题的,但实际使用中发现,只要是通过语音指令模拟发送的键值,目标应用都无法监听到,而通过遥控器直接操作或者adb命令操作都没问题,这个就很奇怪了???
后面通过log分析看到当前KeyEvent接收的应用包名是上面集成的第三方语音服务的,这就清楚了,语音交互的时候有个弹窗,遥控事件被弹窗拦截了。
解决办法有个方案:
1.延时发送模拟键值,等弹窗消失后再发送
2.通过发送广播方式进行通信,目标应用接收广播后处理。
下面给出一份从应用侧到系统侧的「Android 焦点窗口变化」完整链路梳理,覆盖触发时机、WMS/SurfaceFlinger/InputDispatcher 三大核心模块的协作过程,并补充常被忽略的细节(多屏、IME、无焦点 ANR 等)。
一、名词澄清
• 焦点窗口(Focused Window):InputDispatcher 在派发 KeyEvent 时唯一的目标窗口,与 View 体系中“焦点 View”不是一个层次的概念。
• TopFocusedDisplay:RootWindowContainer.mTopFocusedDisplayId,决定了当 KeyEvent 未指定屏幕时由哪块屏接收。
二、触发场景
窗口增删/可见性变化:relayoutWindow()、removeWindow()、finishDrawingWindow()。
切换 Activity/Dialog 弹出。
多屏切换:setFocusedDisplay()。
强制清除焦点:turnScreenOff(), 锁屏。
输入法窗口显隐:InputMethodManagerService 调用 WMS.updateFocusedWindowLocked(UPDATE_FOCUS_PLACING_SURFACES)。
三、核心流程(一次典型的焦点切换)
阶段 1:WMS — “选拔”焦点窗口
① 入口:WMS.updateFocusedWindowLocked(mode, updateInputWindows)
② RootWindowContainer 遍历 DisplayContent,调用 dc.updateFocusedWindowLocked()
③ DisplayContent.findFocusedWindowIfNeeded() 按 Z-order 自上而下过滤:
• 可见、非悬浮错误类型、非 SYSTEM_OVERLAY、不被遮挡
• 若存在焦点 Activity(mAppToken.containsFocusedWindow),则返回其主窗口
• 若无 Activity 状态正常,则选顶部可聚焦窗口
④ 若 newFocus==mCurrentFocus,直接返回;否则把 mCurrentFocus 更新为 newFocus 并记录日志。
⑤ getInputMonitor().setInputFocusLw(newFocus, updateInputWindows) 把结果同步给 InputDispatcher。
⑥ 其他副作用:
• 调整 IME target、toast 超时、Task 阴影、SystemUI 可见性。
阶段 2:SurfaceFlinger — 仅做事务封装
• InputMonitor 通过 SurfaceFlinger 的 createInputWindow/Transaction 把 InputWindowHandle 传给 InputDispatcher。
• 新版本使用 gui::WindowInfosUpdate 批量传输,减少 Binder 次数。
阶段 3:InputDispatcher — “选举结果”生效
① onWindowInfosChanged() → setInputWindowsLocked() 更新 mWindowHandlesByDisplay。
② setFocusedWindow():
• FocusResolver.updateFocusedWindow() 返回 FocusChanges{oldFocus, newFocus, displayId, reason}。
• onFocusChangedLocked():
- 若 oldFocus 存在,向旧窗口发送 CANCEL_NON_POINTER_EVENTS,并 enqueueFocusEvent(oldToken, false)。
- 向新窗口 enqueueFocusEvent(newToken, true)。
• mLooper->wake() 触发下一次 pollOnce,立即派发 FocusEvent。
阶段 4:应用进程 — 收到焦点变更
• Java ViewRootImpl 收到 FocusEvent → DecorView.onWindowFocusChanged() → Activity.onWindowFocusChanged() → 开发者可重写。
• UnityPlayer、FlutterEngine 等 Native 引擎通过监听 windowFocusChanged 决定是否继续渲染或暂停音频。
四、无焦点 ANR 的形成
• InputDispatcher 在派发 KeyEvent 时若 mFocusedWindowTokenByDisplay[displayId] 为空且 5 s 内仍无窗口获得焦点,触发 “Reason: Waiting because no window has focus …” ANR。
五、多屏场景补充
• 每块 DisplayContent 维护独立的 mCurrentFocus。
• RootWindowContainer.updateTopFocusedDisplay() 在每次焦点窗口变化后重新计算 mTopFocusedDisplayId:
– 若某屏有 Activity 含有顶部可见窗口 → 选该屏;
– 否则选默认内屏。
六、关键类/文件速查
WMS:WindowManagerService.java、RootWindowContainer.java、DisplayContent.java、InputMonitor.java
Input:InputDispatcher.cpp、FocusResolver.cpp、InputWindowInfo.h
SurfaceFlinger:SurfaceFlinger.cpp(createInputWindow、Transaction)
七、一句话总结
“焦点窗口”的更新是一场跨进程接力:
WMS 负责“选拔” → SurfaceFlinger 负责“运输” → InputDispatcher 负责“生效”,
最终让正确的窗口在 KeyEvent 到达时“独占”输入事件。