ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AOSP14_物流PDA扫码_03_系统侧监听触发并仲裁及去抖与连按过滤

AOSP14_物流PDA扫码_03_系统侧监听触发并仲裁及去抖与连按过滤 AOSP 14 源码实战物流 PDA 扫码输入统一与 HAL 模拟三—— 系统侧监听触发并仲裁及去抖 / 连按过滤系列第三篇。上一篇我们已经让系统看见了这个键PhoneWindowManager里能打到SCAN_TRIGGER received日志。但那只是一个临时验证日志它证明不了任何事——按一次有两条日志快速按两次会有四条谁也没在管。这一篇要做的就是把这个触发信号收敛成一个唯一的、有判断能力的入口谁可以按、多久之内算重复、什么是抖动、什么是长按自动重复全部在这一层裁决完然后再往下发。本篇只做到「去抖/连按过滤通过 / 被拦截」为止。调用扫码 HAL、获取解码结果、投递给 App 都是后面章节的内容本篇不涉及。一、本篇要解决的问题上一篇结束时留下的问题是按键虽然被识别了但它是裸的。把临时日志拿掉之后任何一次按键都会原封不动地往下传。放到物流现场会立刻碰到三类问题现场问题具体表现机械抖动扳机是金属触点一次按下在电气上会产生若干次通断驱动如果不过滤系统层会收到好几对 down/up长按自动重复按住不放时Linux input 子系统会按input的 repeat 规则持续上报同一按键表现为repeatCount递增一次长按变成几十次触发多入口竞争如果每个业务 App 都自己去监听这个键谁先收到谁就扫码可能出现两个 App 同时开一组会话的互相打架对应的本篇要做三件事仲裁在系统侧建立一个唯一入口把这个按键return 0消费掉业务层再也收不到它——扫码的触发权归系统独有去抖用时间戳做窗口过滤掉触点抖动导致的密集重复按下连按过滤识别并丢弃内核上报的自动重复事件。二、为什么要唯一入口把策略从应用层收上来上一篇的形态 按键 → PWM 拦截 →打条日志→ 继续往下 → 每个 App 都能监听到 本篇的形态 按键 → PWM 拦截 → ScanTriggerManager 裁决 → return 0 消费 │ ▼ 唯一出口后续接 HAL关键点在于return 0返回 0 表示这个按键已经被消费InputDispatcher 不会再把它分发给任何应用于是业务 App根本看不到扫码键也就没法自己在 Activity 里dispatchKeyEvent抢着处理。这一步把谁有权发起一次扫码的决策权收拢到了系统侧。带来的好处是物理策略与业务解耦去抖时长、最短按住时长这类参数写在系统里升级 ROM 就能调不依赖每个 App 改代码不会重复开会话一次触发只会产生一路下行调用不会出现两个 App 同时让 HAL 开两次流水的情况换按键不动业务后面如果要把某个组合键也映射成SCAN_TRIGGER只要改 .kl逻辑入口还是这一个。本篇没有引入新的进程、新的 Binder 服务因此依旧不需要改动 SELinux和上一篇一样纯 system_server 内部逻辑。SELinux 要到后续引入 HAL 独立进程时才会变成必答题。三、先分清两个概念去抖 ≠ 连按过滤这两个词经常被混着说但它们针对的是完全不同的现象判断依据也不一样去抖Debounce连按过滤Auto-repeat Filter针对现象触点电气抖动 / 人手误连带按住不放后内核的自动重复上报事件特征是成对的 down-up-down-up每次repeatCount都是 0repeatCount从 1 开始递增判断依据时间距上一次松开的间隔是否过短事件的repeatCount字段过滤粒度丢弃掉窗口内的整次按下丢弃掉同一次按住过程中的后续重复代价会限制最高触发频率两次有效触发间隔 ≥ DEBOUNCE_MS无副作用一句话区分去抖是时间维度的连按过滤是事件属性维度的。两者必须都做。只做去抖长按会持续触发只做连按过滤电子抖动产生的多个独立 down 依旧会被认为是多次扫码。四、一个容易被忽略的关键为什么用event.getEventTime()而不是System.currentTimeMillis()这是本篇我认为最值得单独拿出来说的一个决定。去抖需要比较两个时间戳本能的写法是记录System.currentTimeMillis()。但在interceptKeyBeforeQueueing这个位置不能这么写原因有两个4.1getEventTime()是事件发生的时间currentTimeMillis()是处理到这里的时间事件从驱动上报到被PhoneWindowManager处理中间经过了 EventHub 读取、InputReader 加工、队列等待等若干环节。System.currentTimeMillis()取到的是我处理你的这一刻它包含了所有排队和处理延迟而KeyEvent.getEventTime()携带的是驱动记录该事件的时刻。用处理时刻做去抖等于把系统的卡顿情况也算进了用户的按键间隔里设备繁忙时两次真实间隔 500ms 的按键可能被你们算成 800ms反过来也可能被算成 100ms 而误杀。4.2eventTime单调递增墙钟时间不一定System.currentTimeMillis()是墙钟时间NTP 校时、用户改表都可能让它回拨。用它算now - last DEBOUNCE_MS在时间被回拨时结果会变成负数直接穿透过滤窗口而在插跳变时又会误判。eventTime取自SystemClock.uptimeMillis()这一类单调时钟不会出现这种问题。4.3 本次实测的印证本次用getEventTime()计算出的按住时长09-27 06:42:19.369 ScanTrigger: valid trigger down, start scan 09-27 06:42:19.478 ScanTrigger: released, hold109mshold109ms和注入脚本两条命令之间的实际节奏是吻合的——说明这个时间戳确实反映了按键本身的时序而不是系统处理的节奏。⚠️ 诚实标注我在本次实现里没有做getEventTime()vscurrentTimeMillis()的对照实验上面的分析是基于两者的语义差异给出的选型理由。它不是实测结论是设计理由。实测结论只有第三节那张日志里展现的第二次按下被拦截这一条。五、实战ScanTriggerManager—— 一个带状态机的裁决器5.1 在哪里建这个类新建目录与文件和上一篇介绍的ScanService同包后续会串起来frameworks/base/services/core/java/com/android/server/scan/ScanTriggerManager.java5.2 完整实现packagecom.android.server.scan;importandroid.os.Handler;importandroid.os.Looper;importandroid.util.Log;/** * 扫码扳机事件去抖 / 连按过滤整系统仅此一个触发入口。 */publicclassScanTriggerManager{privatestaticfinalStringTAGScanTrigger;privatestaticfinallongDEBOUNCE_MS300;// 松开后多久内的按下算抖动privatestaticfinallongMIN_HOLD_MS0;// 最短有效按住时长0不限制privatefinalHandlermHandlernewHandler(Looper.getMainLooper());privatefinalScanTriggerCallbackmCallback;privatebooleanmPressedfalse;privatelongmDownTime0;privatelongmLastUpTime0;publicinterfaceScanTriggerCallback{voidonScanTriggerDown();// 有效按下开始扫描voidonScanTriggerUp();// 松开结束/提交扫描}publicScanTriggerManager(ScanTriggerCallbackcallback){mCallbackcallback;}publicvoidhandleTrigger(booleandown,longeventTime,intrepeatCount){if(!down){if(!mPressed)return;// 防 UP 多报mPressedfalse;longholdeventTime-mDownTime;mLastUpTimeeventTime;Log.d(TAG,released, holdholdms);if(MIN_HOLD_MS0holdMIN_HOLD_MS){Log.d(TAG,hold too short, ignore);return;}if(mCallback!null)mCallback.onScanTriggerUp();return;}if(repeatCount0){// 连按过滤自动重复Log.d(TAG,ignore auto-repeat, repeatCountrepeatCount);return;}if(mPressed)return;// 已在按下中重复 DOWN 忽略if(eventTime-mLastUpTimeDEBOUNCE_MS){Log.d(TAG,debounce: too soon after last up, ignore);return;}mPressedtrue;mDownTimeeventTime;Log.d(TAG,valid trigger down, start scan);if(mCallback!null)mCallback.onScanTriggerDown();}}5.3 三个状态变量变量含义作用mPressed当前是否处于已按下未松开挡重复的 DOWN、挡多报的 UPmDownTime本次按下的时刻算按住时长holdmLastUpTime上一次松开的时刻算去抖窗口eventTime - mLastUpTime这三个变量构成了最小可用的一阶状态机mPressed管我在不在一次会话里两个时间戳管这次会话与上次会话的距离。5.4 四道闸门事件进来要连续过四道闸门任意一道不通过就丢弃#闸门条件挡掉什么1自动重复repeatCount 0→ 丢弃按住不放时内核的连续上报2重复按下mPressed true→ 丢弃没有配对 UP 就又来一个 DOWN异常序列3去抖窗口eventTime - mLastUpTime DEBOUNCE_MS→ 丢弃触点抖动 / 手抖导致的二次按下4防 UP 多报UP 时mPressed false→ 直接返回没有配对 DOWN 的孤立 UP另外还有一个可选的第五道MIN_HOLD_MS—— 按住时长不足则本次完整会话作废。本篇设为 0不限制因此没有实测数据它是为真实硬件预留的现场如果发现轻碰触发了误扫把这个值调到 50~80ms 就能过滤掉。5.5 关于那个mHandler细心的读者会注意到类里声明了mHandler但本篇没用。它是给后面章节预留的符号位判断通过后要把动作切到主线程时用它。另外一个实现层面的说明interceptKeyBeforeQueueing是在输入管线的回调线程上执行的本次日志里 TID 是 576而 system_server 主线程是 490并且总是在同一条线程上串行进来。所以mPressed / mDownTime / mLastUpTime这几个字段没有加锁是安全的。如果后续要把它改成多线程调用必须先补锁。这一点是我基于代码路径和日志做出的判断本篇没有额外做并发压测去证明。六、接入PhoneWindowManager6.1 改动位置frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java先把上一篇那段临时日志换掉改成把事件交给ScanTriggerManagerimportcom.android.server.scan.ScanTriggerManager;privateScanTriggerManagermScanTriggerManager;if(keyCodeKeyEvent.KEYCODE_SCAN_TRIGGER){if(mScanTriggerManagernull){mScanTriggerManagernewScanTriggerManager(newScanTriggerManager.ScanTriggerCallback(){OverridepublicvoidonScanTriggerDown(){Log.i(ScanTrigger, start scan (HAL hook));// TODO: 后续在此调用扫码 HAL 开始扫描}OverridepublicvoidonScanTriggerUp(){Log.i(ScanTrigger, stop/commit scan (HAL hook));// TODO: 后续在此调用扫码 HAL 结束并取解码结果}});}mScanTriggerManager.handleTrigger(down,event.getEventTime(),event.getRepeatCount());return0;// 消费该按键不再下发给应用}6.2 三个传参为什么这么选mScanTriggerManager.handleTrigger(down,event.getEventTime(),event.getRepeatCount());参数为什么传它down区分按下 / 松开状态机的输入边沿event.getEventTime()而不是System.currentTimeMillis()理由见第四节event.getRepeatCount()连按过滤的唯一依据直连内核上报语义6.3 回调里为什么是空的onScanTriggerDown / Up现在只打日志——这是刻意的。本篇的验证目标是第四次这些方法被正确调用了几次而不是它们接下来要做什么。留成 hook下一篇直接往里填 HAL 调用即可本篇的判断逻辑完全不用动。实现上的一个小提示mScanTriggerManager采用了懒加载第一次按键时才 new。功能上没问题因为它总是在同一个线程里被首次触达。更规范的做法是在 PWM 的初始化阶段创建本篇为了改动集中、便于回退选择了前者。七、编译与验证7.1 编译make-j8本篇没有新增任何公开 API既没改KeyEvent.java也没加 Binder 接口因此不会再遇到上一篇那个 metalavacheck current API报错。7.2 模拟两次快速按下用 shell 循环连续注入两组完整的 按下松开foriin12;do\sendevent /dev/input/event131881;sendevent /dev/input/event13000;\sendevent /dev/input/event131880;sendevent /dev/input/event13000;\donelogcat-d|grep-iEScanTrigger7.3 实测结果09-27 06:42:19.369 490 576 D ScanTrigger: valid trigger down, start scan 09-27 06:42:19.369 490 576 I ScanTrigger: start scan (HAL hook) 09-27 06:42:19.478 490 576 D ScanTrigger: released, hold109ms 09-27 06:42:19.478 490 576 I ScanTrigger: stop/commit scan (HAL hook) 09-27 06:42:19.588 490 576 D ScanTrigger: debounce: too soon after last up, ignore7.4 逐条解读把日志按时间轴排开19.369 ↓ DOWN → 通过四道闸门 → valid trigger down → start scan 19.369 紧接着触发 down 回调 19.478 ↑ UP → hold 19.478 - 19.369 109ms → released 19.478 触发 up 回调 19.588 ↓ DOWN → 第 3 道闸门拦截debounce → ignore关键结论两次注入只有一次有效触发—— 全文只有一条valid trigger down第二次被debounce: too soon after last up, ignore挡掉了证明去抖确实生效拦截窗口计算正确—— 第二次按下距上次松开19.588 - 19.478 110ms小于DEBOUNCE_MS 300ms落在窗口内符合预期hold109ms说明时间戳取自事件本身—— 和注入脚本的节奏一致说明getEventTime()反映的是按键而非处理延迟全文只有一条 start scan—— 说明 down/up 与两个回调严格配对没有重复回调、也没有多余回调所有日志的 TID 都是 576—— 印证了interceptKeyBeforeQueueing固定在非主线程的同一条线上串行进来。7.5 一个必须说清的边界这次验证只能证明「触发裁决」这一层是对的它证明不了HAL 有没有被调用下一篇才接有没有解码结果回来下一篇才接结果有没有投递给正确的 App再后面一篇。换句话说本篇的成功严格定义为 —— 该丢的丢掉了该放的放进来了。八、本篇改动清单frameworks/base/services/core/java/com/android/server/scan/ScanTriggerManager.java # 新建 frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java # 改接入 ScanTriggerManager只有两个文件其中一个还是新建的。没有改 SELinux没有新增 Binder 服务没有碰公开 API。九、注意事项与可打磨的地方#项说明1必须用event.getEventTime()用System.currentTimeMillis()会把处理延迟和墙钟回拨引入去抖判断见第四节2去抖窗口会限制最高频率DEBOUNCE_MS300意味着理论最快约 3 次/秒。这个值我没有做真实硬件的抖动波形测量是按常见扳机抖动经验取的真机建议先用getevent抓实际波形再定3MIN_HOLD_MS本篇为 0即不限制按住时长本篇没有启用它因此这块没有实测数据4状态字段未加锁依赖interceptKeyBeforeQueueing单线程串行进来的前提若将来改多线程调用必须补锁5mScanTriggerManager懒加载功能正确但更规范的位置是 PWM 的初始化阶段6return 0是把双刃剑它保证了只有系统能触发扫码同时也意味着兜底方案如果后续裁决逻辑出 bugApp 侧将无法自行接管这个键。建议在下篇接 HAL 时同步加异常兜底日志十、小结与下一篇这一篇完成了整条链路的第二段扳机按下 → /dev/input/eventX → InputReader → .kl → KEYCODE_SCAN_TRIGGER → PhoneWindowManager 拦截 → ScanTriggerManager 裁决 → 去抖窗口 / 连按过滤 / 重复 DOWN / 孤立 UP 四道闸门 → return 0 消费按键业务 App 收不到 → onScanTriggerDown / onScanTriggerUp 两个干净的 hook核心收获理解了**去抖时间维度与连按过滤事件属性维度**是两件不同的事必须都做掌握了为什么去抖要用event.getEventTime()——避免把排队延迟和墙钟跳变算进按键间隔用mPressed / mDownTime / mLastUpTime三个字段搭出了最小可用的按键状态机通过return 0消费按键把谁有权触发扫码的决策权收归系统侧用一次真实的快速连按注入亲眼验证了两次只出一次有效触发。下一篇四把那两个空着的 hook 填起来——编写 legacy HAL 模块scan.default.so用 JNI 打通 “Java 系统服务 → HAL”让 start scan之后真的有码值产生出来。未完待续 · 第四篇调用扫码 HAL 执行采集与解码
返回列表