拿到一台MTK平台的Android 10.0板子,客户提了个需求:侧键要做成相机功能键,按一下打开相机,再按一下拍照。听起来很简单对吧?不就是把侧键的键值映射成KEYCODE_CAMERA,再让相机App处理一下快门事件嘛。可真上手之后你会发现,这件事从内核驱动到Framework再到App,整整串了四五层,任何一层没配合好,按键都会像石沉大海一样毫无反应。
这篇文章我就把自己在MTK 10.0上把侧键调成相机功能键的完整过程拆开来讲,包括需求定义、按键事件链路、.kl映射改法、Framework截获快门事件、以及各种"改了没反应"的坑。适合做方案定制、系统集成的工程师,也适合想搞懂"安卓按键到底是怎么流转的"的App开发同学。
1. 拿到需求先别改代码:把"相机功能键"拆成几种行为
很多人接定制需求的第一反应就是"改个键值映射就行了",但"相机功能键"这句话太模糊了,不同产品对它的预期完全不一样。我先花点时间问清楚了四种常见行为,再决定改动路径。
1.1 四种常见行为定义
第一种是"快捷启动相机":任何界面下按一下侧键,直接拉起系统相机,这是最容易实现的,本质是一个全局快捷键。
第二种是"相机内按一下拍照":相机界面下,侧键变成快门,按下触发对焦,松开拍照,或者按下直接拍。这一层需要Camera App配合处理KeyEvent,或者由Framework直接把快门事件塞给Camera应用。
第三种是"息屏直接拍":屏幕关着、系统锁着,长按或连按侧键直接打开相机并拍照。这个改动会牵扯到锁屏、Doze模式、唤醒逻辑,复杂度和前两种不是一个量级。
第四种是"特殊手势组合":比如同时按侧键和音量下键截屏,或者长按侧键录视频。这基本就要走Framework或PMS层的key intercept逻辑了。
回到我手上这个需求,客户描述是"侧键做相机功能键,按一下打开相机,在相机里按一下拍照,再按一下或者按返回退出"。这就是第一和第二种的组合,不涉及息屏直拍,我松了一口气。但即使如此,真正落地时依然踩了好几个坑。
1.2 MTK平台为什么要单独讨论
MTK和Qualcomm在按键处理上有个明显区别:MTK平台很多非标准按键(侧键、功能键、NFC快捷键之类的)往往不是走Linux标准的gpio-keys驱动,而是挂在PMIC或者EINT中断上,由MTK的misc和input子模块上报。这就导致一个问题:你以为改了Android的kl文件就有用,实际上内核端可能压根没把物理按键报成Linux键值,或者报成了一个奇怪的扫描码。
这也是为什么网上一搜"MTK按键进入拍照",会浮出一堆跟驱动、dts配置、按键上报有关的帖子。MTK的按键个性化定制,起步阶段先要确定物理按键在哪个input设备上、以什么scancode上报,然后再谈Android层的键值映射,否则后面全是空中楼阁。
2. 按键从硅片到App的完整旅程:MTK上报链路的来龙去脉
在动手之前,我觉得有必要把"按下侧键"到"相机快门被触发"这条链路完整过一遍。搞不清楚这条链路,改错层的代价就是反复编译、反复刷机、反复无效。
2.1 物理到内核:GPIO/EINT与Linux input keycode
侧键物理上是一个轻触开关,按下后把某个GPIO拉低或拉高,MTK芯片通过EINT(外部中断)感知电平变化,然后由平台驱动上报一个键值。比如常见的侧键可能上报KEYCODE_F1(59)或KEYCODE_F2(60),有的模组干脆恶心一点,上报成一些多媒体键或未知扫描码。
MTK平台定制按键,早期方案喜欢在dts里加gpio-keys节点:
&keypad { key_side { label = "Side key"; linux,code = <KEY_F1>; gpios = <&pio 78 GPIO_ACTIVE_LOW>; gpio-key,wakeup; debounce-interval = <5>; }; };不过MTK 10.0的板子很多是直接把按键走到PMIC的pressed/key中断里,不一定在dts里看得到。所以最可靠的方式是先拿到root shell,直接getevent看物理键到底报了哪个input event,这一步能省后面80%的自我怀疑。
2.2 InputReader与.kl映射:Android这边先认个脸
内核把键值报上来之后,Android的InputReader会读取/dev/input/下的event设备,然后用.kl文件把Linux键值映射成Android层的KeyEvent。这个映射文件在AOSP里叫Generic.kl,MTK设备上通常是mtk-kpd.kl、mt8167-kpd.kl之类的,路径一般在/system/usr/keylayout/或/vendor/usr/keylayout/。
典型的一行长这样:
key 59 CAMERA意思就是"Linux键码59映射成Android的KEYCODE_CAMERA(27)"。注意,第2列的CAMERA是Android的KeyCode标签,不是最终KeyEvent的数字值,最终值在KeycodeLabels.h或InputEventLabels.h里查。
表:我这次用到的最关键映射
| Linux键码 | kl标签 | Android KeyEvent | 用途 |
|---|---|---|---|
| 59 | F1 | KEYCODE_F1(131) | 侧键原始上报 |
| 60 | F2 | KEYCODE_F2(133) | 侧键原始上报 |
| 27 | CAMERA | KEYCODE_CAMERA(27) | 相机快门 |
| 80 | FOCUS | KEYCODE_FOCUS(80) | 对焦 |
| 24 | VOLUME_UP | KEYCODE_VOLUME_UP(24) | 音量上/拍照 |
2.3 快速定位当前键值和无头绪时的诊断命令
如果你不确定手上的板子侧键当前上报的是什么,用这三板斧最快:
# 1. 看哪些input设备是按键设备 adb shell dumpsys input # 2. 监听原始键码 adb shell getevent -l # 3. 看当前Klayout加载信息 adb shell dumpsys input | grep -A 5 "KeyLayoutFile"我在一台MT6765板子上实测,侧键被内核报成了KEY_F1(scancode 59),Android默认把59映射成KEYCODE_F1,所以App收到的就是KEYCODE_F1,跟拍照毫无关系。定位到这第一步,后面的改动就很清晰了。
3. 第一步改动:把侧键在系统中注册成KEYCODE_CAMERA
既然内核已经把侧键报成59了,Android层的第一步就是改映射,把59从F1改成CAMERA。
3.1 修改.kl文件与KeycodeLabels的步骤
MTK 10.0设备上,我习惯先查设备的kl文件是哪个:
adb shell su 0 "ls /system/usr/keylayout/" adb shell su 0 "ls /vendor/usr/keylayout/"找到对应kl后,修改59那一行:
key 59 CAMERA注意不要直接改AOSP的Generic.kl,MTK设备上很可能会被平台自己的kl覆盖,改了Generic.kl等于白改。应该改设备具体加载的那个kl,或者新建一个Vendor_XXXX_Product_XXXX.kl,让InputReader按vendor/product匹配。
改完后还有一步很多人漏掉:如果打算新增一个"Android KeyCode标签",需要在framework/native/include/android/keycodes.h或KeycodeLabels.h里同步加,但因为我们用的是现成的CAMERA,压根不需要加新标签,所以官方用kl映射就够了。
3.2 改完没反应?常见的原因清单
改kl后没反应,我列一下我在MTK平台上遇过的可能原因,按概率排序:
- kl文件被SELinux策略挡了,加载失败。这种情况
dumpsys input会打印找不到对应字库文件或拒绝读取。 - 改错了kl文件。系统实际加载的是另一个设备的kl,你改的是Generic.kl。
- 设备事件没走InputReader,被MTK的
mtk-kpd驱动半路截住了。 - 改了文件但没重启system_server。kl映射是在InputReader初始化时加载的,必须重启UI进程。
我的做法是先临时push一个kl文件到/data/local/tmp,再用adb shell input keyevent 59做一次软件注入测试,如果软件注入能被识别为CAMERA,但物理按键不行,那就多半是驱动层或input设备匹配的问题。
3.3 一个很多人忽略的厂商分区问题
Android 10的MTK平台普遍有system和vendor分区只读保护,user版本还带dm-verity。你想直接adb remount改kl,大概率会失败。
adb root adb disable-verity adb reboot adb remount这条路在MTK userdebug上偶尔可行,但如果是user版本,就得老老实实编译system.img或vendor.img再刷机。MTK平台刷机也不要乱来,OTA的logo.bin和vendor分区变动都会导致开机异常,我遇到过一版改错vendor导致开机卡在logo的情况,后来用SP Flash Tool强刷才救回来。
所以正规做法是:在源码里改vendor kl文件,编vendor.img,用update或fastboot刷vendor分区,不要图快直接改system。毕竟侧键做相机功能键只是一个小需求,搞到变砖就得不偿失了。
4. 真正难的不是键值,是App的拍照交互
把侧键映射成KEYCODE_CAMERA之后,我以为到了拍照这一步很简单——Camera App接到KEYCODE_CAMERA自然会拍照。结果一测,按下侧键,相机打开了,但再按侧键,没反应。
4.1 Camera App通常怎么接KeyEvent
AOSP原生相机确实处理了KEYCODE_CAMERA,但很多三方相机App、厂商定制相机、甚至一些轻量版相机App,压根没有处理这个键。它们的拍照逻辑只监听两种KEYCODE:KEYCODE_VOLUME_UP和KEYCODE_VOLUME_DOWN,因为音量键兼容性最好、最无脑,甚至有的ROM里连屏幕上的拍照按钮都不是App自己画的,是SystemUI加的一个Activity。
所以你的Camera App到底会不会响应KEYCODE_CAMERA,必须先确认。最简单的方法:
adb shell input keyevent 27如果相机界面下发了27没反应,就是App没处理这个KeyEvent。这时候有三条路:
- 改Camera App源码,加
KEYCODE_CAMERA的onKeyDown/onKeyUp逻辑。 - 在Framework层把
KEYCODE_CAMERA转换成别的键事件,比如转换成App已经处理过的KEYCODE_VOLUME_DOWN或KEYCODE_FOCUS。 - 在Framework层干脆不再往下发,直接改由Camera应用代码通过Camera2 API触发拍照。
4.2 焦点问题与事件被吞的现象
即使App处理了KEYCODE_CAMERA,还有一个"焦点陷阱":按键事件默认先派发给当前窗口,如果当前界面不是Camera Activity而是桌面、系统UI,那事件根本到不了Camera Activity。
更恶心的是时序问题:按下侧键,你拦截KeyEvent并启动CameraActivity,但Activity还没onResume,此时第二次按侧键,事件可能被SystemUI或Keyguard吃掉。这也是为什么"按一下打开相机"和"在相机里按一下拍照"是两个感受完全不同的需求,前者是全局事件,后者要求焦点必须在Camera App上。
我曾经为了处理这个时序问题,在Framework的PhoneWindowManager.interceptKeyBeforeDispatching里做状态机:第一次按侧键,启动相机;记录时间戳,300ms内收到第二次ActionDown,就派发给相机;超过300ms,当作新的启动请求。实测下来比无脑拦截好用很多。
4.3 为什么"音量键拍照"在部分手机上失效
顺带说一下很多人问的事:为什么有的手机上音量键能拍照,有的不能?原因还是在App和Framework两层。
App层处理KEYCODE_VOLUME_DOWN拍照,同时Framework层又把KEYCODE_VOLUME_DOWN当成系统音量下降来处理。如果Framework在dispatch之前拦截了音量键,App永远收不到。AOSP在PhoneWindowManager里有音量键的特殊逻辑:如果当前窗口是MediaSession要响应音量,PMS会直接把事件消费掉。
从这件事可以引出一个结论:做侧键相机功能键,最好的姿势不是在App层蹭事件,而是先在Framework定格清楚——这个键是"系统级快门"还是"App级快门"。如果是系统级快门,Framework拦截后启动Camera并发送快门KeyEvent,或直接用RemoteControlClient/MenuAction的方式通知App;如果只是App级快门,就做好焦点管理,别让别的界面抢事件。
5. Framework层截获方案:让侧键在系统里"说了算"
考虑到兼容性和可控性,我最后的方案选了Framework截获。原因很简单:客户的三方相机App不可能允许我改源码,我不能指望App去响应KEYCODE_CAMERA,只能让系统统一处理快门事件。
5.1 什么时候必须走Framework
你是AOSP源码方案商、像我们一样的系统定制方,手里握着framework源码,走Framework拦截几乎是一劳永逸的选择。具体来说有几种需求必走Framework:
- 息屏/锁屏下长按侧键打开相机并拍照;
- 任何界面下单侧键立即打开相机,哪怕当前处于全屏游戏;
- 既要有"打开相机"又要有"在相机内拍照",且不允许出现"打开瞬间又拍照"的误触发;
- 需要同时绑定多个手势(长按、双击、组合键)。
5.2 截获KeyEvent并启动相机的实现逻辑
以AOSP 10的PhoneWindowManager.java为例,核心逻辑在interceptKeyBeforeDispatching:
@Override public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event, int policyFlags) { final int keyCode = event.getKeyCode(); final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; if (keyCode == KeyEvent.KEYCODE_CAMERA) { if (down && event.getRepeatCount() == 0) { boolean cameraActive = isCameraActive(); if (!cameraActive) { launchCamera(); mSideKeyDownTime = event.getEventTime(); return -1; // 拦截,不派发 } } if (event.getAction() == KeyEvent.ACTION_UP) { // 如果相机已激活且当前时间接近按下时间,避免误触发 long duration = event.getEventTime() - mSideKeyDownTime; if (duration < 350) { // 视为同一个按下周期,不再重复启动 } } } return super.interceptKeyBeforeDispatching(focusedToken, event, policyFlags); }启动相机的Intent有两个选择:MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA(android.media.action.STILL_IMAGE_CAMERA)和直接指定包名。前者会走系统默认相机,后者适合客户明确要某个特定相机App。
我倾向用前者,因为客户可能换相机App,用系统标准Intent更健壮。
5.3 拦截与分发之间的时序细节
拦截方案写起来不难,难在时序。我在MTK 10.0上遇到过"按下侧键相机打开了,但松手的一瞬间又触发了一次截屏/返回"的问题,原因就是ACTION_UP没有拦干净,事件漏给SystemUI处理了。
所以Framework拦截时必须成对处理Down和Up,两个都拦住。不然就会出现:ACTION_DOWN启动相机,ACTION_UP被SystemUI识别成返回键或截屏键,用户操作体验很分裂。
另外,如果是做息屏直拍方案,还要联动PowerManager.wakeUp()和KeyguardManager,先亮屏再启动相机。MTK平台在这一步也要注意,个别板子在doze状态下按键唤醒只走power键通路,其他按键的上报路径会被PMIC关断,处理起来跟普通唤醒不一样,需要跟MTK原厂确认平台行为。
我这次需求不涉及息屏直拍,所以主动放弃了这部分,只是在代码里留了KEYCODE_CAMERA从锁屏启动相机的处理,没有做完整唤醒链路。
6. 实机验证与踩坑排查链路
改完代码、编完bin、刷上机之后,我以为大功告成,结果花在调试上的时间比写代码还多。这一节我把完整的排查链路和几个值得记录的坑分享出来。
6.1 一个完整的排查流程
拿到一台刷好包的MTK 10.0机器,侧键相机功能键异常时,我按这个顺序查:
第一步,确认物理键有没有上报:
adb shell getevent -l按下侧键,看有没有/dev/input/eventX和对应的KEY_F1/KEY_CAMERA输出。如果没有,问题在驱动或dts,改Android层没用。
第二步,确认Android层收到什么键值:
adb shell getevent -l adb shell input keyevent 27软注入27如果能触发热键,说明系统层映射正常;如果不触发,问题在PMS拦截逻辑或Camera App接收逻辑。
第三步,看焦点和窗口状态:
adb shell dumpsys window | grep -E "mCurrentFocus|mFocusedApp" adb shell dumpsys input确认按下侧键时当前焦点窗口是哪个,CameraActivity是否真的resume了。
第四步,抓event log和system log:
adb logcat -b events | grep -i keyevent adb logcat -b system | grep -i PhoneWindowManager看KeyEvent有没有被PMS拦截,拦截后有没有执行到launchCamera。
这个流程基本能把99%的"侧键相机键失灵"问题定位到具体层。我也建议你在自己的板子上把这套排查顺序固定下来,别一上来就怀疑kl映射,很多问题根本轮不到kl出场。
6.2 几个我在MTK机器上遇到的经典坑
坑一:改了kl但不生效,原因是MTK加载了vendor kl,而不是system Generic.kl。这种情况改Generic.kl没用,确认一下dumpsys input | grep -A 5 "KeyLayoutFile"。
坑二:相机打开了,但"打开瞬间又拍了一张照",原因是Framework没有拦截ACTION_UP,Common的相机会把这次Up也当成快门响应。在PMS里对同一个KeyEvent周期内的Down/Up做配对消耗即可。
坑三:侧键映射成CAMERA之后,三方相机依然没反应,但音量键拍照正常。验证方法:在相机界面执行adb shell input keyevent 24(音量上),如果能拍照但27不能,就不要纠缠了,直接改成Framework把相机侧的侧键事件翻译成音量快门或发送MediaAction。
坑四:MTK平台按键设备和触屏设备事件挤压,InputReader在同一时间片内处理多个事件,侧键快速连按时漏事件。这个我们后来通过抓dumpsys input发现事件其实进了队列,只是App侧onKeyDown回调太慢丢了,最后是让Camera App用HandlerQueue串行处理KeyEvent解决。
坑五:user版本无法remount,修改验证效率极低。以后凡是涉及framework改动,建议一开始就用userdebug版开发,或者配置好adb disable-verity流程,否则每次验证都像在原地踏步。
6.3 顺手整理一份调试常用命令
我把这次用得最多的调试命令整理成一个备忘,你直接抄:
# 查看按键布局加载情况 adb shell dumpsys input | grep -E "KeyLayoutFile|KeyCharacterMap" # 查看当前焦点窗口 adb shell dumpsys window | grep -E "mCurrentFocus|mFocusedApp" # 查看input设备列表 adb shell getevent -i # 实时查看事件 adb shell getevent -l # 演示按键注入 adb shell input keyevent 27 adb shell input keyevent 24 # 查看PMS拦截日志 adb logcat -b system | grep PhoneWindowManager # 查看KeyEvent分发日志 adb logcat -b events | grep -E "key|AM_ACTIVITY"这套命令在MTK和Qualcomm平台上都能用,不挑平台,建议保存。以后做类似的功能键定制,打开日志的瞬间你就能判断出问题在哪一层,不用瞎猜。
我自己在侧键做相机功能键这个需求上摸爬滚打后最大的体会是:Android的按键机制从Linux input到Klayout再到InputDispatcher,是一环扣一环的链条,想走捷径跳层改动大概率会引入更诡异的问题。真正靠谱的打法是先把"这个键在当前产品里应该是什么行为"定义清楚,再从驱动往上逐层确认没断点,最后才做Framework或App的处理。如果你也是做MTK平台定制,建议把这套链路里的日志命令记下来,下一个功能键需求你一定用得上。