华为鸿蒙把 AccessibilityService 折腾成什么样了

2026-03-28

今天做了一个给老人用的语音助手 App——说一句"给儿子发微信说我吃饭了",自动完成整个流程。目标很简单,但华为鸿蒙把我教做人了。


项目背景

App 叫 ElderPlay,核心逻辑:

  1. 按住悬浮按钮说话(离线语音识别,sherpa-onnx + SenseVoice)
  2. LLM 解析意图,提取联系人名和消息内容
  3. AccessibilityService 自动操作微信完成发送

设备:HUAWEI BAH3-W59(MatePad T8),HarmonyOS,API 29。


第一坑:rootInActiveWindow 跨 App 失效

最早的多轮 Agent 方案:每轮读屏幕节点 → 喂给 LLM → LLM 决定下一步操作。在华为上,一旦切到微信,rootInActiveWindow 就返回 null 或空节点,LLM 收到空屏幕,开始乱猜联系人名字("彭xx"这个真实联系人名完全读不到,模型自己编了一个)。

修复:改用 getWindows() 遍历所有窗口,跳过输入法窗口和自身包名:

val wins = windows
for (win in wins) {
    if (win.type == AccessibilityWindowInfo.TYPE_INPUT_METHOD) continue
    val root = win.root ?: continue
    val pkg = root.packageName?.toString() ?: ""
    if (pkg == "com.elderplay.agent") { root.recycle(); continue }
    traverseNode(root, nodes)
    root.recycle()
}

这个改了之后,节点确实能读到了,但后来发现这只是开始。


第二坑:LLM 看不到屏幕,坐标乱点

读到了节点,但 LLM 没有图像,只有文字描述,让它估坐标就是瞎猜。Agent 连续把"xxx前端交流群"和"遵义路xxx互助团购群"当成了目标联系人打开。

于是转向截图方案:让 LLM 看截图再决定操作。


第三坑:截图 API 一个都用不了

方案 A:AccessibilityService.takeScreenshot() 需要 API 30+,设备是 API 29,直接跳过。

方案 B:Bitmap.wrapHardwareBuffer() 需要 API 31+,同上。

方案 C:MediaProjection API 21+ 都能用。实现了,加了"开启屏幕录制"按钮,用户点击允许后……App 直接崩了。华为对 MediaProjection 的处理明显有问题,createVirtualDisplayImageReader 一帧都拿不到,超时5秒返回 null,全部12轮都是"截图不可用"。

最终:截图方案在这台华为设备上全部失败


转折:放弃通用 Agent,改写死流程

发送微信消息这件事,流程是固定的:

打开微信 → 点搜索 → 输入联系人名
→ 读取搜索结果节点 → 让用户确认联系人
→ 点击联系人 → 输入消息 → 点发送

不需要 LLM 来导航,LLM 只做一件事:从语音里提取 contactmessage

// CommandParser:一次 LLM 调用
// "跟彭xx说我吃过了" → {"intent":"wechat_send","contact":"彭xx","message":"我吃过了"}

when (cmd.intent) {
    "wechat_send" -> WeChatSender.send(contact, message, ...)
}

WeChatSender 是8步硬编码流程,每步都是确定性的代码,不再有 LLM 参与导航。


第四坑:ACTION_PASTE 在微信搜索框失效

硬编码流程跑起来了,微信搜索框也打开了,键盘也弹出来了,剪贴板也写进去了(键盘上方能看到"彭xx"的剪贴板建议)。但就是粘不进去。

ACTION_PASTE 返回 false,ACTION_SET_TEXT 也不行。

搜了一下,发现各品牌的限制对比,总结如下:

问题类型华为小米OPPO/VIVOPixel
rootInActiveWindow 跨 App受限基本可用基本可用完全可用
ACTION_PASTE通常可用,需正确聚焦可用可用完全可用
takeScreenshot()受限可用(API30+)可用(API30+)完全可用
服务保活极难(PowerGenie)较难较难无限制

ACTION_PASTE 在华为上理论上可用,但要求节点真正聚焦。问题可能是调用顺序——要先 ACTION_FOCUS 再粘贴。这个还在调试中。


今天的教训

  1. 国产 ROM 的限制不在 API 层,在进程保活和 window 访问层rootInActiveWindow 不是被禁了,而是 window 焦点判断逻辑不同。

  2. 通用 LLM Agent 对固定流程场景是过度设计。12 轮循环 + LLM 决策,不如8行硬编码可靠。

  3. 截图方案对旧华为设备完全不可行,不是 API 限制,是硬件兼容性问题。

  4. 剪贴板粘贴是中文输入的最佳方案,但依赖节点的聚焦状态,细节很重要。


下一步

搞定 ACTION_PASTE 的聚焦问题(加 ACTION_FOCUS 或长按触发粘贴菜单),然后完整跑通"给彭xx发我吃过了"这个流程。

或许真搞不定了。这个计划就流产了。