ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

AOSP14_物流PDA扫码_02_InputReader与kl键码映射

AOSP14_物流PDA扫码_02_InputReader与kl键码映射 AOSP 14 源码实战物流 PDA 扫码输入统一与 HAL 模拟二—— InputReader 与 .kl 键码映射系列第二篇。上一篇铺了整体链路这一篇开始真正改源码攻链路的第一段扳机按下 → Linux input 事件 → InputReader → 通过 .kl 映射为统一 KeyCode。本篇只做到「系统能识别并拦截到这个统一的扫码键」为止。去抖 / 连按过滤、系统服务仲裁、HAL 调用都属于后面章节本篇不涉及。一、本篇要解决的问题回顾上一篇的痛点不同厂商 PDA 的扫码扳机上报的键值五花八门。有的映射成 F12scancode 88有的是厂商自定义键有的走 GPIO 或串口。业务 App 为了适配只能在代码里堆if-else。我们这一篇要做的事就是在系统层把这件事一次性收敛掉硬件上报任意 scancode ──► .kl 映射 ──► 统一的 KEYCODE_SCAN_TRIGGER ──► 系统层识别无论硬件上报什么进入 Android 框架后都变成同一个 KeyCode。业务层只需要认KEYCODE_SCAN_TRIGGER这一个键。二、先理清扳机按下后事件是怎么进入 Android 的2.1 Linux input 子系统与/dev/input/eventXAndroid 建立在 Linux input 子系统之上。硬件按键包括 PDA 的扫码扳机在内核里都会注册成一个 input 设备用户空间通过/dev/input/eventX这样的字符设备节点读取事件。三个必须先搞清楚的概念概念含义scan code硬件上报的原始扫描码。由硬件/驱动决定同一颗芯片在不同机型上可能不同EV_KEY按键事件类型value1表示按下value0表示松开EV_SYN同步帧SYN_REPORT标记一组事件的结束没有它框架不会把事件当作一次完整的输入用getevent可以直观看到内核原始事件adb shell getevent-l/dev/input/event13# EV_KEY KEY_F12 DOWN# EV_SYN SYN_REPORT 00000000# EV_KEY KEY_F12 UP# EV_SYN SYN_REPORT 000000002.2 从内核节点到 InputReader/dev/input/eventX │ EventHub 通过 inotify epoll 监听设备节点读出原始事件 ▼ EventHub → RawEvent此时还是 scancode │ InputReader 把 RawEvent 交给对应的 InputMapper ▼ KeyboardInputMapper │ 查 .klKeyLayoutMap把 scancode 翻译成 keycode ▼ NotifyKeyArgs此时已是 Android 的 keycode │ ▼ InputDispatcher → PhoneWindowManager 拦截 → 应用本篇最关键的一条分界线InputReader从内核拿到的仍然是scancode硬件语言。把 scancode 翻译成keycodeAndroid 语言这一步靠的是.kl键布局文件。也就是说.kl 就是解决键值不一致的核心武器。硬件差异在这一层被吸收掉往上的框架和 App 看到的都是统一的 keycode。2.3 没有真实硬件怎么办真实 PDA 的扳机会由驱动上报 EV_KEY而模拟器AOSP emulator / Cuttlefish没有物理扳机但输入子系统是真的我们可以用sendevent往 input 节点直接注入 EV_KEY 事件效果等同于按下扳机。这一点上一篇已经铺垫过。三、为什么是「新增一个 KeyCode」而不是复用现有的一个自然的想法是扳机上报的不就是 F12 吗直接监听KEYCODE_F12不就行了我最终没有这么做原因有两个语义冲突KEYCODE_F12在 Android 里有它自己的含义。如果业务层监听 F12 就当扫码那么用户真的按了键盘 F12 时会被误判成扫码。无法统一A 厂商上报 F12、B 厂商上报自定义键、C 厂商上报别的——如果复用现有 keycode不同机型映射到的 keycode 还是不一样的收敛不了。所以正确做法是新增一个语义明确的专用 KeyCode——KEYCODE_SCAN_TRIGGER让所有机型的 .kl 都把各自的 scancode 映射到它。这样业务层只认一个键语义明确不会误判机型差异被 .kl 吸收换机型只改 .kl不动代码。四、实战新增 KeyCode 的「四件套」在 AOSP 里新增一个 KeyCode必须同时改四个文件缺一不可。它们分别处在 native 层和 Java 层构成一条完整的绑定链。4.1 第一步native 层定义数值frameworks/native/include/android/keycodes.hAKEYCODE_SCAN_TRIGGER317这是整个链条的源头Android keycode 的本质就是一个 int。这里选317是因为它落在 AOSP 预留给厂商/自定义的区间内避免和现有键冲突。4.2 第二步注册「字符串 ↔ 数值」的标签映射frameworks/native/libs/input/InputEventLabels.cpp找到KEYCODES_SEQUENCE宏序列在末尾追加DEFINE_KEYCODE(SCAN_TRIGGER),\这个宏的定义是#defineDEFINE_KEYCODE(key){#key,AKEYCODE_##key}它的作用是把字符串SCAN_TRIGGER和数值AKEYCODE_SCAN_TRIGGER绑定在一起。为什么必须有这一步因为.kl文件里写的是字符串SCAN_TRIGGER而框架内部用的是 int。InputEventLabels.cpp就是这两者之间的字典——没有它.kl 里写key 88 SCAN_TRIGGER框架根本不认识这个名字。这也是getevent -l、dumpsys input、key命令等工具能把数值打印成人话的原因。4.3 第三步Java 层定义常量frameworks/base/core/java/android/view/KeyEvent.javapublicstaticfinalintKEYCODE_SCAN_TRIGGER317;这一步让 Java 侧App、PhoneWindowManager能用KeyEvent.KEYCODE_SCAN_TRIGGER来引用它。⚠️这一步会触发一个编译报错见第六节。4.4 第四步attrs.xml 补充枚举声明frameworks/base/core/res/res/values/attrs.xmlenumnameKEYCODE_SCAN_TRIGGERvalue317/attrs.xml里的keycode枚举用于 XML 层面例如布局/配置里引用按键的合法性校验。补上它这个新 keycode 在 XML 体系里才是合法值。四件套小结文件作用缺失后果keycodes.h定义 int 数值整个链条没有源头InputEventLabels.cpp字符串 ↔ 数值绑定.kl 里的名字解析不了KeyEvent.javaJava 常量Java 侧引用不到attrs.xmlXML 枚举声明XML 里引用不合法五、实战.kl 文件完成 scancode → keycode 映射5.1 .kl 是什么.klKey Layout File是按键布局文件作用就是一行一条映射规则key scancode keyCode例如我们这次用的key 88 SCAN_TRIGGER含义scancode 为 88 的按键翻译成SCAN_TRIGGER这个 keycode。88 是我这台模拟器上用来模拟扳机的那个 scancode真实 PDA 上要换成你扳机实际的上报值可以用getevent抓出来。5.2 .kl 的加载两个需要知道的规则AOSP 在查找某个 input 设备该用哪个.kl时有几条规则。这里我只写和本次实战相关的两条并标注哪些是本次实测过的规则 A文件名按设备名匹配且会去掉尾部数字系统会根据 input 设备的名称去找同名的.kl。并且在匹配时设备名尾部的数字会被去掉例如设备名qwerty2会先尝试qwerty2.kl找不到再退化成qwerty.kl。说明本次实战没有专门针对这条规则做对比实验。我采取的做法是——直接给新建的文件起一个明确的名字vendor_pda_product_code.kl不带数字后缀并改已经确定在用的qwerty.kl从命名上绕开了这个歧义。所以这条属于实现时需要知道的规则不是本次实测结论。规则 B分区优先级/vendor高于/system同一个.kl名字在多个分区都存在时vendor 分区下的会优先生效。说明本次实战没有做同名的 system 份 vs vendor 份的对比实验所以我不在这里下实测证明 vendor 优先的结论。本次实际观察到的现象是模拟器键盘加载的是/vendor/usr/keylayout/qwerty.kl见 5.4所以我最终改的是 vendor 下那一份。5.3 面向真实硬件新建 .kl 并预置到系统在device/generic/goldfish/keylayout/下新建qwerty2.kl内容key 88 SCAN_TRIGGER然后在device_common.mk末尾把它预置进系统PRODUCT_COPY_FILES \ device/generic/goldfish/keylayout/qwerty2.kl:$(TARGET_COPY_OUT_SYSTEM)/usr/keylayout/vendor_pda_product_code.kl注意这里的源文件名和目标文件名不一样源叫qwerty2.kl安装到设备上叫vendor_pda_product_code.kl。这样命名是给将来真实硬件用的——真实设备上报时系统会按设备 ID 的命名方式去匹配对应的键布局文件这里我们预留了一个语义清晰的名字。5.4 ⚠️ 本次踩到的第一个真坑模拟器上改的那份根本不是生效的那份做完 5.3 我以为映射就生效了编译启动后注入按键——没有任何反应。排查后发现没有真实硬件输入节点时我用的是键盘输入事件来模拟而模拟器键盘加载的键布局文件是 vendor 分区下的/vendor/usr/keylayout/qwerty.kl并不是我在 5.3 里预置到/system/usr/keylayout/的那份。定位到它是被这一行拷贝进去的# device/generic/goldfish/vendor_common.mk:307 device/generic/goldfish/input/qwerty.kl:$(TARGET_COPY_OUT_VENDOR)/usr/keylayout/qwerty.kl \对应的源码路径是device/generic/goldfish/input/qwerty.kl。于是我在这份文件末尾追加同样的映射key 88 SCAN_TRIGGER这一处是本篇最值得记的坑.kl不是加了就生效而是具体某个 input 设备按名字匹配到具体某一个文件。在模拟器上用键盘模拟时生效的是 vendor 下那份qwerty.kl你往 system 下预置的新文件键盘根本不会去读它。排查这类问题的实用手段dumpsys input可以看到每个 input 设备实际加载的.kl/.kcm路径比猜要快得多。六、编译以及第二个坑metalava API 检查失败四件套 .kl 改完执行全编make-j86.1 报错现象[ 17% 1641/9174] //frameworks/base/api:api-stubs-docs-non-updatable check current API [common] FAILED: .../check_current_api.timestamp --- frameworks/base/core/api/current.txt .../api-stubs-docs-non-updatable_api.txt -50981,6 50981,7 public class KeyEvent extends android.vi field public static final int KEYCODE_S 47; // 0x2f field public static final int KEYCODE_SCAN_TRIGGER 317; // 0x13d field public static final int KEYCODE_SCROLL_LOCK 116; // 0x74 ****************************** You have tried to change the API from what has been previously approved. To make these errors go away, you have two choices: 1. You can add hide javadoc comments (...). 2. You can update current.txt and/or removed.txt by executing: m api-stubs-docs-non-updatable-update-current-api ******************************6.2 原因因为我在KeyEvent.java里加了一个public static final常量——这属于公开 API 变更。AOSP 用 metalava 做 API 管控任何公开 API 的增删改都必须和已归档的current.txt快照一致否则直接构建失败。这个报错不是代码写错了而是你改了公开 API 但没走流程。6.3 解法二选一报错信息里已经给了两个选项方案做法适用场景加hide在常量上加/** hide */让它不进公开 SDK这个键只给系统内部用不对外暴露更新 API 快照m api-stubs-docs-non-updatable-update-current-api确认它就是公开 API 的一部分本次我选的是更新快照m api-stubs-docs-non-updatable-update-current-api执行后再make -j8编译通过。补一句这两个方案在要不要对外暴露上是有区别的。如果你的 KeyCode 只想给系统服务用、不想让第三方 App 依赖加hide更干净。本项目里我按公开发布处理所以走了更新快照这条路。七、验证让系统「看见」这个键7.1 加一段临时日志仅为验证映射在PhoneWindowManager的按键分发前拦截点加日志。frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java在interceptKeyBeforeQueueing中if(keyCodeKeyEvent.KEYCODE_SCAN_TRIGGER){Log.d(ScanTrigger,SCAN_TRIGGER received, downdown, repeatCountevent.getRepeatCount());return0;}⚠️说明这里只是为了验证映射是否成功而打的临时日志。真正的拦截处理、去抖、连按过滤属于下一篇系统服务层的内容本篇不做。7.2 启动模拟器并注入按键emulator-verbose-writable-systemadb logcat-c# 模拟扳机按下 松开EV_KEY code 88sendevent /dev/input/event131881# EV_KEY, code88, value1 → 按下sendevent /dev/input/event13000# SYN_REPORT → 同步帧sendevent /dev/input/event131880# EV_KEY, code88, value0 → 松开sendevent /dev/input/event13000# SYN_REPORT → 同步帧7.3 结果adb logcat-d|grep-iEScanTrigger|SCAN_TRIGGER09-27 03:30:50.963 485 576 D ScanTrigger: SCAN_TRIGGER received, downtrue, repeatCount0 09-27 03:31:08.991 485 576 D ScanTrigger: SCAN_TRIGGER received, downfalse, repeatCount0按下和松开各一条说明scancode 88 已经被.kl正确翻译成KEYCODE_SCAN_TRIGGERInputEventLabels.cpp的字符串绑定生效了否则.kl里的名字解析不了事件一路走到了PhoneWindowManager并被拦截return 0表示消费掉不再下发给应用。7.4 一个值得强调的结论这一步纯粹是静态配置只改了源文件里的映射表和几个常量定义没有引入任何新的运行时机制也不需要配置 SELinux。在SELinux Enforcing强制模式下映射与拦截正常工作。这一点很重要它说明.kl映射属于标准的输入子系统行为不涉及跨进程、不涉及新增服务因此不会被 SELinux 拦。后面几篇引入系统服务、HAL 进程、Binder 之后SELinux 才会变成必须处理的问题。八、本篇改动清单frameworks/native/include/android/keycodes.h # AKEYCODE_SCAN_TRIGGER 317 frameworks/native/libs/input/InputEventLabels.cpp # DEFINE_KEYCODE(SCAN_TRIGGER) frameworks/base/core/java/android/view/KeyEvent.java # KEYCODE_SCAN_TRIGGER 317 frameworks/base/core/res/res/values/attrs.xml # enum ... value317 / frameworks/base/core/api/current.txt # 由 update-current-api 自动更新 device/generic/goldfish/keylayout/qwerty2.kl # 新建key 88 SCAN_TRIGGER device/generic/goldfish/device_common.mk # 预置到 /system/usr/keylayout/ device/generic/goldfish/input/qwerty.kl # 追加key 88 SCAN_TRIGGER模拟器实际生效的那份 frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java # 临时验证日志九、本篇踩坑汇总#坑现象解法1改的 .kl 不是生效的那份加完映射注入按键没任何反应用dumpsys input确认设备实际加载的.kl路径模拟器键盘用的是/vendor/usr/keylayout/qwerty.kl要改它的源文件device/generic/goldfish/input/qwerty.kl2metalava API 检查失败新增public常量后构建失败报 “tried to change the API”m api-stubs-docs-non-updatable-update-current-api或给常量加hide3漏改 InputEventLabels.cpp.kl里写了 keycode 名字但解析不了映射静默失效四件套必须齐全DEFINE_KEYCODE是字符串→数值的唯一字典十、小结与下一篇这一篇完成了整条链路的第一段扳机按下 → /dev/input/eventX (EV_KEY) → EventHub → InputReader → .kl 把 scancode 翻译成 KEYCODE_SCAN_TRIGGER → PhoneWindowManager 拦截到核心收获理解了scancode 与 keycode 的分界——.kl 正是解决键值不一致的地方掌握了新增一个 KeyCode 的完整四件套及其各自的作用认识到.kl 是具体设备匹配具体文件不是加了就生效了解了 AOSP 的metalava 公开 API 管控机制。下一篇三进入系统服务层——把这个 KeyCode 的监听收敛到ScanTriggerManager做两层去抖down repeatCount 0event.getEventTime()时间戳比对、状态机管理mPressed/mDownTime/mLastUpTime并解释为什么用event.getEventTime()而不是System.currentTimeMillis()。未完待续 · 第三篇系统服务拦截与去抖
返回列表