Lenovo TB371FC 手写笔 attach 抖动的软件修复实战
本文记录一次 Lenovo TB371FC / Android 14 平板的系统级排障过程:手写笔充满电后持续触发“拿起、放下”逻辑,导致原厂手写笔悬浮球反复闪动,锁屏后设备也会被不断唤醒。
最终修复不是关闭蓝牙规避,而是在 framework 层修改 services.jar 中的 Lenovo 手写笔策略类:
对底层笔吸附状态加入 1500ms 防抖确认。
过滤不稳定的 attach/detach 抖动。
去掉笔 attach/detach 与锁屏唤醒的强绑定。
保留原厂 com.lenovo.penservice、悬浮球、电量和笔服务能力。
最终产物:
1 2 3 4 5 远端工作目录:hualai@192.168.1.248:~/Share/stylus-debug Magisk 模块:parsec_monitor_input v0.3 替换文件:/system/framework/services.jar 最终 services.jar sha256: 6af5896af2bcacfa7fc4de2f71f7abda64ec8ac029f99e9dd4565425851a3af4
1. 问题背景
设备:
1 2 3 4 型号:Lenovo TB371FC 系统:Android 14 / API 34 调试主机:hualai@192.168.1.248 ADB 设备:HA1YX1J7
故障现象:
手写笔充满电后,即使没有真实拿起/放下,也会不断触发笔吸附状态变化。
原厂悬浮球反复显示和隐藏,看起来像一直闪。
设备锁屏后会被反复唤醒,无法稳定保持息屏。
关闭蓝牙后现象消失,但这会牺牲蓝牙笔的电量、按键、远程控制等功能,也影响其他蓝牙设备。
一开始可以用关闭蓝牙作为临时止血:
1 2 adb shell su -c 'svc bluetooth disable' adb shell input keyevent KEYCODE_SLEEP
但这个方案本质是绕开问题,不是修复问题。真正要解决的是:底层 attach/detach 抖动不应该直接广播给上层服务,更不应该直接唤醒屏幕。
2. 日志定位:问题不在触摸事件,而在笔吸附状态
先在远端主机确认设备连接:
1 2 ssh hualai@192.168.1.248 adb devices -l
确认设备是 TB371FC:
1 HA1YX1J7 device usb:... product:TB371FC_PRC model:TB371FC device:TB371FC
抓取相关日志:
1 2 adb logcat -c adb logcat -v time | grep -Ei 'BluetoothManagerPolicy|PEN_ATTACH_CHANGED|LenovoIME|PenBattery|FloatingCollapseView|wakeUp'
关键日志模式如下:
1 2 3 4 5 6 7 8 9 10 D/BluetoothManagerPolicy: onUeventReceved: address 00:00:00:00:00:00, battery level null, chargeState Unknown, isPenAttached 0, mCurrentPenAttached 1 I/PenBatteryControllerImpl: Receive Intent ACTION = lenovo.intent.action.PEN_ATTACH_CHANGED I/LenovoIME: Take off pen, then show floatview D/BluetoothManagerPolicy: onUeventReceved: address 00:00:00:00:00:00, battery level null, chargeState Unknown, isPenAttached 1, mCurrentPenAttached 0 I/LenovoIME: attach the pen, then hide floatview
这说明问题链路是:
1 2 3 4 5 底层 sysfs / uevent 报告 attached 0/1 抖动 -> framework 中 Lenovo BluetoothManagerPolicy / BluetoothPenConnectPolicy 接收状态 -> 发送 lenovo.intent.action.PEN_ATTACH_CHANGED 广播 -> com.lenovo.penservice / LenovoIME 显示或隐藏 FloatingCollapseView -> 锁屏状态下还会走 PowerManager.wakeUp
所以这不是普通 MotionEvent、触摸屏、手写输入坐标的问题,而是系统 framework 中的手写笔吸附状态策略问题。
3. 为什么选择改 services.jar
尝试过几种方向:
关闭 com.lenovo.penservice:可以阻止悬浮球闪,但原厂笔服务也没了。
关闭蓝牙:可以阻止 attach 抖动进入蓝牙笔逻辑,但功能损失太大。
只改设置项:无法阻断系统层 PEN_ATTACH_CHANGED 和唤醒路径。
改 SystemUI:不是主因,风险更大。
日志里真正的源头类名是:
1 com.zui.server.input.styluspen.pen.bluetooth.policy.BluetoothPenConnectPolicy
这个类位于 /system/framework/services.jar 的 dex 中。它负责处理笔 attach/detach uevent,并继续广播给上层组件。所以最小有效修改点就是 services.jar。
4. 提取 services.jar
在远端主机建立工作目录:
1 2 3 ssh hualai@192.168.1.248mkdir -p ~/Share/stylus-debugcd ~/Share/stylus-debug
从设备拉取 framework jar:
1 2 3 adb root 2>/dev/null || true adb pull /system/framework/services.jar ./services.jarsha256sum services.jar
原始 services.jar 记录:
1 f121281a575d9479ef6810ff909d0d30f57dcb419b77302229fac9487099a6c7 services.jar
由于 services.jar 本质是一个 zip/jar 容器,里面有多个 dex:
1 2 3 4 mkdir -p dexcd dex jar xf ../services.jarls -lh classes*.dex
本次目标类在 classes3.dex 中。
5. 准备 smali / baksmali 工具
工作目录中使用的是 smali 3.0.9 相关 jar:
1 2 3 4 smali-tools/smali-3.0.9.jar smali-tools/smali-baksmali-3.0.9.jar smali-tools/smali-dexlib2-3.0.9.jar ...
反编译目标 dex:
1 2 3 4 5 cd ~/Share/stylus-debugmkdir -p smali-classes3 java -jar smali-tools/smali-baksmali-3.0.9.jar disassemble \ dex/classes3.dex \ -o smali-classes3
定位目标类:
1 find smali-classes3 -path '*BluetoothPenConnectPolicy.smali' -print
目标文件:
1 smali-classes3/com/zui/server/input/styluspen/pen/bluetooth/policy/BluetoothPenConnectPolicy.smali
备份原始 smali:
1 2 cp smali-classes3/com/zui/server/input/styluspen/pen/bluetooth/policy/BluetoothPenConnectPolicy.smali \ BluetoothPenConnectPolicy.smali.before_nowake
6. 找到需要修改的方法
用日志关键字和方法名定位:
1 2 3 4 5 grep -n 'onUeventReceved' \ smali-classes3/com/zui/server/input/styluspen/pen/bluetooth/policy/BluetoothPenConnectPolicy.smali grep -n 'PENATTACHED\|PENNOTATTACHED\|PEN_ATTACH_CHANGED\|wakeUp' \ smali-classes3/com/zui/server/input/styluspen/pen/bluetooth/policy/BluetoothPenConnectPolicy.smali
核心方法是:
1 onUeventReceved(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
它会:
读取 uevent 上报的笔状态。
判断 isPenAttached 与 mCurrentPenAttached 是否不同。
更新当前状态。
在 attach/detach 时调用 PowerManager.wakeUp(...)。
构建并发送 PEN_ATTACH_CHANGED 广播。
这也是为什么日志里一旦底层 attached 在 0/1 之间抖动,上层就会一直收到“拿起/放下”。
7. 第一个补丁:去掉 attach/detach 唤醒
第一版修复先处理锁屏唤醒。原始逻辑中 attach 和 detach 分支会调用:
1 2 PowerManager.wakeUp(..., "PENATTACHED") PowerManager.wakeUp(..., "PENNOTATTACHED")
在 smali 里把这两个 wakeUp 调用替换为跳转,让流程不再实际唤醒屏幕:
1 2 - invoke-virtual {v3, v4, v5, v2, v6}, Landroid/os/PowerManager;->wakeUp(JILjava/lang/String;)V + goto/16 :goto_b7
这个补丁可以解决“锁屏后不断亮屏”的核心问题,但它还没有解决亮屏状态下悬浮球闪动。因为只要 attach/detach 广播仍然发出去,com.lenovo.penservice 就会继续显示/隐藏悬浮球。
8. 第二个补丁:在 onUeventReceved 中加入 1500ms 防抖
最终方案是在 onUeventReceved(...) 的 attach 状态处理前加入防抖。
逻辑是:
如果新上报的状态与 mCurrentPenAttached 一致,说明没有真实变化,继续原流程。
如果新状态与当前状态不一致,先等待 1500ms。
等待后重新读取真实状态文件:
1 /sys/devices/virtual/misc/ctn730/attached
如果 1500ms 后状态已经变回去了,认为这次是抖动,直接丢弃。
如果状态稳定,才继续更新 mCurrentPenAttached 并发送广播。
对应插入的 smali 关键逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 iget v3, p0, Lcom/zui/server/input/styluspen/pen/bluetooth/policy/BluetoothPenConnectPolicy; ->mCurrentPenAttached:Iif-eqz v0, :debounce_desired_detached if-ne v3, p3, :debounce_changed goto :debounce_done:debounce_desired_detached if-nez v3, :debounce_changed goto :debounce_done:debounce_changed const-wide/16 v3, 0x5dc:try_start_debounce invoke-static {v3, v4}, Ljava/lang/Thread; ->sleep(J)V:try_end_debounce .catch Ljava/lang/InterruptedException; {:try_start_debounce .. :try_end_debounce } :catch_debounce goto :debounce_after_sleep:catch_debounce invoke-static {}, Ljava/lang/Thread; ->currentThread()Ljava/lang/Thread; move-result-object v3invoke-virtual {v3}, Ljava/lang/Thread; ->interrupt()V:debounce_after_sleep const-string v3, "/sys/devices/virtual/misc/ctn730/attached" invoke-virtual {p0, v3}, Lcom/zui/server/input/styluspen/pen/bluetooth/policy/BluetoothPenConnectPolicy; ->getFileValue(Ljava/lang/String; )Ljava/lang/String; move-result-object v3const-string v4, "1" invoke-virtual {v4, v3}, Ljava/lang/String; ->equals(Ljava/lang/Object; )Zmove-result v4if-eq v4, v0, :debounce_done ; 状态不稳定,记录日志并跳过 attach 变化处理goto/16 :cond_e0:debounce_done
这里的 0x5dc 是十六进制的 1500,也就是 1500ms。
最终 diff 的关键部分如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @@ -3889,6 +3889,76 @@ const/4 v2, 0x0 + iget v3, p0, Lcom/zui/server/input/styluspen/pen/bluetooth/policy/BluetoothPenConnectPolicy;->mCurrentPenAttached:I + ... + const-wide/16 v3, 0x5dc + invoke-static {v3, v4}, Ljava/lang/Thread;->sleep(J)V + ... + const-string v3, "/sys/devices/virtual/misc/ctn730/attached" + invoke-virtual {p0, v3}, ...->getFileValue(Ljava/lang/String;)Ljava/lang/String; + ... + if-eq v4, v0, :debounce_done + ... + goto/16 :cond_e0 + + :debounce_done if-eqz v0, :cond_71
为什么选择 1500ms:
太短,比如 100-300ms,可能挡不住充满电后的机械/驱动抖动。
太长,比如 3-5s,会让真实拿起/放下反馈明显变慢。
1500ms 是一个偏保守但仍可接受的折中点。如果后续仍偶发闪动,可以提升到 2000-3000ms。
9. 重新编译 dex
修改 smali 后重新编译 classes3.dex:
1 2 3 4 5 6 cd ~/Share/stylus-debug java -jar smali-tools/smali-3.0.9.jar assemble \ smali-classes3 \ -o classes3.debounce.dexsha256sum classes3.dex classes3.debounce.dex
本次记录:
1 2 bb202b2143cc7b1728f72d703ba0e1ad013b8efcf9a11d060b5cf8a10b6edc24 classes3.dex 33c8d163c6d0c4adb147eb203de4dabdd2d38f2f87ee18ac196ced5effb8475b classes3.debounce.dex
10. 重新打包 services.jar
不要只把一个 dex 单独推到设备。services.jar 需要保持 jar 容器结构,把原 jar 的其他 dex 和 META-INF 一起保留,只替换 classes3.dex。
准备重打包目录:
1 2 3 4 5 6 7 cd ~/Share/stylus-debugrm -rf jar-debouncemkdir -p jar-debouncecd jar-debounce jar xf ../services.jarcp ../classes3.debounce.dex classes3.dex jar cf ../services.debounce.jar .
校验:
1 2 3 cd ~/Share/stylus-debug jar tf services.debounce.jar | grep '^classes' sha256sum services.debounce.jar
最终 services.debounce.jar:
1 6af5896af2bcacfa7fc4de2f71f7abda64ec8ac029f99e9dd4565425851a3af4 services.debounce.jar
11. 为什么用 Magisk 模块替换
直接 remount /system 修改 framework 风险高,而且 Android 14 的动态分区、只读分区和 AVB 校验会让直接替换更麻烦。
设备已有 root / Magisk 环境,所以更稳妥的方式是做一个 Magisk 模块,用 systemless overlay 替换:
1 /system/framework/services.jar
模块目录:
1 2 3 4 5 6 magisk-module-debounce/ module.prop customize.sh system/ framework/ services.jar
module.prop 内容:
1 2 3 4 5 6 id =parsec_monitor_input name =Parsec MONITOR_INPUT + stylus debounce framework patch version =0.3 versionCode =3 author =OpenClaw summary =Framework patch for Parsec immersive input plus Lenovo stylus attach debounce/wake suppression
customize.sh 内容:
1 2 3 4 SKIPMOUNT=false PROPFILE=false POSTFSDATA=false LATESTARTSERVICE=false
复制 jar:
1 2 3 cd ~/Share/stylus-debugmkdir -p magisk-module-debounce/system/frameworkcp services.debounce.jar magisk-module-debounce/system/framework/services.jar
打包模块:
1 2 cd ~/Share/stylus-debug/magisk-module-debounce zip -r ../parsec_monitor_input_v0.3_stylus_debounce.zip .
记录 zip:
1 78c057a2a9821030171cbfe6a9f1bd16f2396cfe44b0fa8d9479b4279c93dc26 parsec_monitor_input_v0.3_stylus_debounce.zip
12. 安装 Magisk 模块
推送模块到设备:
1 2 cd ~/Share/stylus-debug adb push parsec_monitor_input_v0.3_stylus_debounce.zip /data/local/tmp/
通过 Magisk 安装:
1 2 adb shell su -c 'magisk --install-module /data/local/tmp/parsec_monitor_input_v0.3_stylus_debounce.zip' adb reboot
重启后检查实际生效文件:
1 adb shell su -c 'sha256sum /system/framework/services.jar'
应当看到:
1 6af5896af2bcacfa7fc4de2f71f7abda64ec8ac029f99e9dd4565425851a3af4 /system/framework/services.jar
13. 恢复原厂笔服务
早期为了止血,曾经临时禁用过 com.lenovo.penservice 和关键组件。framework 层修复完成后,需要恢复它们,否则悬浮球、电量和笔服务不会完整工作。
启用包和组件:
1 2 3 4 5 6 adb shell su -c 'pm enable com.lenovo.penservice' adb shell su -c 'pm enable com.lenovo.penservice/com.lenovo.pen.bt.service.FloatService' adb shell su -c 'pm enable com.lenovo.penservice/com.lenovo.penservice.service.ViewEventService' adb shell su -c 'pm enable com.lenovo.penservice/com.lenovo.penservice.ime.LenovoIME' adb shell su -c 'pm enable com.lenovo.penservice/com.lenovo.penservice.receiver.BootBroadcastReceiver' adb shell su -c 'pm enable com.lenovo.penservice/com.lenovo.penservice.receiver.EasyJotReceiver'
恢复笔相关设置,具体 key 会因系统版本不同而不同,本次做法是把之前关闭的笔设置恢复为 1:
1 2 3 4 adb shell settings put system stylus_floating_ball 1 adb shell settings put system stylus_screen_off_note 1 adb shell settings put system stylus_button_note 1 adb shell settings put system stylus_global_handwriting 1
给悬浮窗权限:
1 adb shell su -c 'appops set com.lenovo.penservice SYSTEM_ALERT_WINDOW allow'
启动悬浮球服务:
1 adb shell su -c 'am startservice -n com.lenovo.penservice/com.lenovo.pen.bt.service.FloatService'
注意不要用:
1 adb shell su -c 'am start-foreground-service -n com.lenovo.penservice/com.lenovo.pen.bt.service.FloatService'
原因是这个服务没有及时调用 startForeground(),Android 会抛出 ForegroundServiceDidNotStartInTimeException,导致悬浮球服务被系统杀掉并重建。
14. 验证修复
14.1 检查服务和窗口
1 2 adb shell dumpsys activity services com.lenovo.penservice | grep -E 'FloatService|ViewEventService' adb shell dumpsys window | grep -E 'FloatingCollapseView|com.lenovo.penservice'
修复后可以看到:
1 2 FloatService 正在运行 WindowManager 中存在 FloatingCollapseView
14.2 检查没有广播刷屏
1 2 3 adb logcat -csleep 15 adb logcat -d | grep -Ei 'PEN_ATTACH_CHANGED|Take off pen|attach the pen|debounce drop|PENATTACHED|PENNOTATTACHED|wakeUp'
预期:
不再出现 PEN_ATTACH_CHANGED 高频刷屏。
不再出现 Take off pen, then show floatview / attach the pen, then hide floatview 循环。
不再出现 PENATTACHED / PENNOTATTACHED 唤醒路径。
如果底层仍抖动,可能看到 debounce drop unstable attach,这说明 framework 防抖正在丢弃不稳定状态。
14.3 锁屏稳定性验证
1 2 3 adb shell input keyevent KEYCODE_SLEEPsleep 30 adb shell dumpsys power | grep -E 'mWakefulness|Display Power'
修复后的验证结果:
设备可以保持息屏,不再被笔 attach/detach 抖动反复唤醒。
15. 回滚方法
如果 framework 补丁导致系统异常,优先从 Magisk 恢复。
方法一:Magisk 管理器禁用模块后重启。
方法二:ADB root 下删除模块目录。模块 id 是 parsec_monitor_input:
1 2 adb shell su -c 'rm -rf /data/adb/modules/parsec_monitor_input' adb reboot
如果设备无法正常进系统,需要进入 Magisk 安全模式或 recovery 环境删除同一目录。
16. 本次排障的关键判断
这次问题能修对,关键是没有停留在“悬浮球闪”这个表象,而是顺着日志往下追:
1 2 3 4 5 FloatingCollapseView 闪动 -> LenovoIME show/hide -> PEN_ATTACH_CHANGED 广播 -> BluetoothPenConnectPolicy.onUeventReceved -> /sys/devices/virtual/misc/ctn730/attached 抖动
如果只处理上层 UI,例如禁用悬浮球、杀掉笔服务,确实能让现象消失,但手写笔功能也会被破坏。
如果只关闭蓝牙,也能让现象消失,但本质是阻断了整个蓝牙笔通道。
更合理的修复点是在 framework 收到 attach 状态后做稳定性确认:真实拿起/放下可以继续广播,短时间抖动则丢弃。这样既保留原厂笔服务,又避免底层噪声直接驱动 UI 和电源管理。
17. 最终状态
最终设备状态:
1 2 3 4 5 6 7 8 蓝牙:开启 com.lenovo.penservice:启用 FloatService:运行 ViewEventService:启用 LenovoIME:启用 SYSTEM_ALERT_WINDOW:allow Magisk 模块:parsec_monitor_input v0.3 framework services.jar:已替换为 debounce 版本
最终效果:
原厂手写笔服务保留。
悬浮球可以显示,并且不再反复闪动。
锁屏后设备可以稳定保持 Dozing。
底层 attach/detach 抖动仍可能存在,但被 framework 层过滤,不再向上层广播成连续“拿起/放下”。
这类问题的通用经验是:遇到厂商定制硬件状态抖动时,先用日志定位广播源头和 framework 类,再判断是否适合做 debounce。不要急着改 UI,也不要把关闭硬件通道当成最终修复。