Android13 按键kl文件优先级详解
本文专门讲解一下Android 按键接收和处理作用的键值kl文件的选择过程,有需要的可以了解。
本文具体逻辑和调试是使用Android13代码和系统。
"idc"(Input Device Configuration)为输入设备配置文件,
它包含设备具体的配置属性,这些属性影响输入设备的行为。对于touch screen设备,总是需要一个idc文件来定义其行为。
"kl"(key layout)文件是一个键值布局映射文件,是标准linux与anroid的键值映射文件,
"kcm"(key code mapping)文件意为按键字符映射文件,作用是将 Android按键代码与修饰符的组合映射到 Unicode字符
本文kl流程分析主要参考:https://blog.csdn.net/kc58236582/article/details/52199274
Android kl(key layout)文件是一个映射文件,是标准linux与anroid的键值映射文件,
kl文件可以有很多个,但是它有一个使用优先级:
/system/usr/keylayout/Vendor_XXXX_Product_XXXX_Version_XXXX.kl
/system/usr/keylayout/Vendor_XXXX_Product_XXXX.kl
/system/usr/keylayout/DEVICE_NAME.kl
/data/system/devices/keylayout/Vendor_XXXX_Product_XXXX_Version_XXXX.kl
/data/system/devices/keylayout/Vendor_XXXX_Product_XXXX.kl
/data/system/devices/keylayout/DEVICE_NAME.kl
/system/usr/keylayout/Generic.kl //**主要使用
/data/system/devices/keylayout/Generic.kl
上面这个优先级也是其他文章提供的,并不准确,估计是分析的代码是Android9或者更早的系统代码。
但是只要你看完后面的分析,自己可以有比较全面认知。
1、通过"getevent"查看事件节点和节点名称;
2、通过"dumpsys input"查看节点的具体使能的kl文件;
130|atom:/ # getevent
//(1)这里可以查看到按键的eventX节点,和节点在内核上的命名名称
add device 1: /dev/input/event2
name: "aw8624_haptic"
add device 2: /dev/input/event0
name: "ACCDET"
add device 3: /dev/input/event3
name: "fts_ts"
add device 4: /dev/input/event1
name: "mtk-kpd"
//(2)按下音量减按键,这里第二列的0001 对应的数据才是有用的数据,可以看到音量减键对应的按键键值是0x72
/dev/input/event1: 0001 0072 00000001 //(3)1是按下
/dev/input/event1: 0000 0000 00000000
/dev/input/event1: 0001 0072 00000000 //(4)0是抬起
/dev/input/event1: 0000 0000 00000000
//(5)按下音量加按键,可以看到音量加键对应的按键键值是0x73
/dev/input/event1: 0001 0073 00000001
/dev/input/event1: 0000 0000 00000000
/dev/input/event1: 0001 0073 00000000
/dev/input/event1: 0000 0000 00000000
上面可以看到节点的名称和某个按键在上层的键值。
atom:/ # dumpsys input
INPUT MANAGER (dumpsys input)
Input Manager State:
Interactive: false
System UI Visibility: 0x8008
Pointer Speed: 0
Pointer Gestures Enabled: true
Show Touches: false
Pointer Capture Enabled: false
Event Hub State: //(1)事件状态信息是主要关注的
BuiltInKeyboardId: -2
Devices: //(2)Devices里面的每个信息都是对应不同的节点信息
-1: Virtual
Classes: 0x40000023
Path: (3)关注Path字符串,就是节点的位置,这里是虚拟,不清楚具体意义
Enabled: true
Descriptor: a718a782d34bc767f4689c232d64d527998ea7fd
Location:
ControllerNumber: 0
UniqueId:
Identifier: bus=0x0000, vendor=0x0000, product=0x0000, version=0x0000
KeyLayoutFile: /system/usr/keylayout/Generic.kl
KeyCharacterMapFile: /system/usr/keychars/Virtual.kcm
ConfigurationFile:
HaveKeyboardLayoutOverlay: false
VideoDevice:
1: aw8624_haptic
Classes: 0x00000200
Path: /dev/input/event2
Enabled: true
Descriptor: 65195a4ab35c59e79bbba55177be90fc42ed3ae6
Location:
ControllerNumber: 0
UniqueId:
Identifier: bus=0x0000, vendor=0x0000, product=0x0000, version=0x0000
KeyLayoutFile:
KeyCharacterMapFile:
ConfigurationFile:
HaveKeyboardLayoutOverlay: false
VideoDevice:
2: ACCDET
Classes: 0x00000081
Path: /dev/input/event0
Enabled: true
Descriptor: 1c78f7e0d16d4dbc8d3ab93943523f379203f90b
Location:
ControllerNumber: 0
UniqueId:
Identifier: bus=0x0019, vendor=0x0000, product=0x0000, version=0x0000
KeyLayoutFile: /system/usr/keylayout/Generic.kl
KeyCharacterMapFile: /system/usr/keychars/Generic.kcm
ConfigurationFile:
HaveKeyboardLayoutOverlay: false
VideoDevice:
3: fts_ts
Classes: 0x00000015
Path: /dev/input/event3
Enabled: true
Descriptor: a1cc21cba608c55d28d6dd2b1939004df0e0c756
Location:
ControllerNumber: 0
UniqueId:
Identifier: bus=0x0018, vendor=0x0000, product=0x0000, version=0x0000
KeyLayoutFile: /system/usr/keylayout/Generic.kl
KeyCharacterMapFile: /system/usr/keychars/Generic.kcm
ConfigurationFile:
HaveKeyboardLayoutOverlay: false
VideoDevice:
4: mtk-kpd //(4)按键事件的节点命名名称
Classes: 0x00000001
Path: /dev/input/event1 //(5)按键事件的节点位置,这个才是主要的,名称可以不看,但是节点必须找对
Enabled: true
Descriptor: f0d2e427e7a05eb6d316f5e14800c5ac7b6aee79
Location:
ControllerNumber: 0
UniqueId:
Identifier: bus=0x0019, vendor=0x2454, product=0x6500, version=0x0010 //(6)各版本号,寻找kl使用到
KeyLayoutFile: /system/usr/keylayout/mtk-kpd.kl //(7)实际起作用的kl文件
KeyCharacterMapFile: /system/usr/keychars/Generic.kcm
ConfigurationFile:
HaveKeyboardLayoutOverlay: false
VideoDevice:
...
上面可以看到某个按键event节点的具体使能的kl和kcm文件。
这里是使用的mtk-kpd.kl 文件,可以看到其他有些event使用的是默认的 Generic.kl 文件。
选择kl文件逻辑都是在cpp文件中的。
framework\native\services\inputflinger\reader\EventHub.cpp
framework\native\libs\input\InputDevice.cpp //kl选择的主要逻辑
framework\native\libs\input\Keyboard.cpp
这个要讲清楚还是比较麻烦的,这里只能介绍中间某一段。
最开始哪里开始和最后哪里结束,还真不好介绍,因为本来对c不熟悉,这里都是强行看到逻辑代码。
建议流程:
EventHub::getEvents
-> EventHub::scanDevicesLocked
-> EventHub::scanDirLocked
-> EventHub::openDeviceLocked
-> EventHub::loadKeyMapLocked
-> Keyboard::KeyMap::load
-> KeyMap::loadKeyLayout
-> KeyMap.getPath(deviceIdentifier, name, InputDeviceConfigurationFileType::KEY_LAYOUT) //加载kl文件
-> InputDevice.getInputDeviceConfigurationFilePathByDeviceIdentifier //文件路径拼接和选择
-> InputDevice.getInputDeviceConfigurationFilePathByName //目录遍历,匹配kl
-> KeyMap::loadKeyLayout //未匹配到kl的情况,继续往下走
-> Keyboard.probeKeyMap(deviceIdentifier, "Generic") // 使用Generic.kl
不同的Android系统可能有差别,我看上面参考网址的是Android6的代码有些定义和现实是不一样的,但是总体流程差不多。
其实懂得大致流程就ok了。具体代码需要自己添加日志打印查看流程,默认打印一点点。
下面是主要过程代码分析:
static const char* DEVICE_INPUT_PATH = "/dev/input"; //系统input子系统目录,按键事件的节点都在这个目录下
//1、这个方法系统系统和每次触发按键都会执行
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
ALOG_ASSERT(bufferSize >= 1);
ALOGI("getEvents "); //2、自己添加的打印,发现系统启动一次和每次按键触摸等事件都有打印
...
for (;;) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
if (mNeedToScanDevices) {
mNeedToScanDevices = false; //3、设置只加载一次配置文件
scanDevicesLocked();//4、扫描加载配置文件
mNeedToSendFinishedDeviceScan = true;
}
}
...
}
//5、扫描加载节点函数
void EventHub::scanDevicesLocked() {
status_t result;
std::error_code errorCode;
if (std::filesystem::exists(DEVICE_INPUT_PATH, errorCode)) {
result = scanDirLocked(DEVICE_INPUT_PATH);//6、扫描目录 "/dev/input"
if (result < 0) {
ALOGE("scan dir failed for %s", DEVICE_INPUT_PATH);
}
} else {
if (errorCode) {
ALOGW("Could not run filesystem::exists() due to error %d : %s.", errorCode.value(),
errorCode.message().c_str());
}
}
。。。
}
//7、扫描加载界面目录函数,有兴趣的可以打印下这个 entry.path() 是否某个某个具体的节点信息
status_t EventHub::scanDirLocked(const std::string& dirname) {
for (const auto& entry : std::filesystem::directory_iterator(dirname)) {
openDeviceLocked(entry.path());
}
return 0;
}
//8、加载界面目录函数,里面具体的判断非常多,这里只展示主要部分
void EventHub::openDeviceLocked(const std::string& devicePath) {
// If an input device happens to register around the time when EventHub's constructor runs, it
// is possible that the same input event node (for example, /dev/input/event3) will be noticed
// in both 'inotify' callback and also in the 'scanDirLocked' pass. To prevent duplicate devices
// from getting registered, ensure that this path is not already covered by an existing device.
ALOGV("Opening device: %s", devicePath.c_str()); // 9、Android13 ALOGV是打印不出来的,如果需要查看信息可以修改为 ALOGI
...
ALOGV("add device %d: %s\n", deviceId, devicePath.c_str()); //这里的ALOGV 也是同理,所以Android13 上加载流程日志是超少的
ALOGV(" bus: %04x\n"
" vendor %04x\n"
" product %04x\n"
" version %04x\n",
identifier.bus, identifier.vendor, identifier.product, identifier.version);
ALOGV(" name: \"%s\"\n", identifier.name.c_str());
// Load the configuration file for the device.
device->loadConfigurationLocked(); //10、这里是加载kcm配置文件的,有兴趣可以看,这里不去追
。。。
// Configure virtual keys.
if ((device->classes.test(InputDeviceClass::TOUCH))) {
// Load the virtual keys for the touch screen, if any.
// We do this now so that we can make sure to load the keymap if necessary.
bool success = device->loadVirtualKeyMapLocked(); // 11、加载虚拟按键配置,这里也不去追
if (success) {
device->classes |= InputDeviceClass::KEYBOARD;
}
}
// Load the key map.
// We need to do this for joysticks too because the key layout may specify axes, and for
// sensor as well because the key layout may specify the axes to sensor data mapping.
status_t keyMapStatus = NAME_NOT_FOUND;
if (device->classes.any(InputDeviceClass::KEYBOARD | InputDeviceClass::JOYSTICK |
InputDeviceClass::SENSOR)) {
// Load the keymap for the device.
keyMapStatus = device->loadKeyMapLocked(); //** 12、这里加载才是加载按键配置
}
}
//13、这里调用的是 Keyboard::KeyMap::load
status_t EventHub::Device::loadKeyMapLocked() {
return keyMap.load(identifier, configuration.get());
}
下面继续追踪Keyboard.cpp的代码。
//1、加载配置
status_t KeyMap::load(const InputDeviceIdentifier& deviceIdentifier,
const PropertyMap* deviceConfiguration) {
// Use the configured key layout if available.
if (deviceConfiguration) {
String8 keyLayoutName;
if (deviceConfiguration->tryGetProperty(String8("keyboard.layout"),
keyLayoutName)) {
status_t status = loadKeyLayout(deviceIdentifier, keyLayoutName.c_str());//2、加载配置具体信息
if (status == NAME_NOT_FOUND) {
ALOGE("Configuration for keyboard device '%s' requested keyboard layout '%s' but "
"it was not found.",
deviceIdentifier.name.c_str(), keyLayoutName.string());
}
}
...
// generic key map to use (US English, etc.) for typical external keyboards.
if (probeKeyMap(deviceIdentifier, "Generic")) { //3、如果没找到kl文件,就是用 Generic.kl,这个文件正常是肯定存在的
return OK;
}
}
//4、加载配置具体信息
status_t KeyMap::loadKeyLayout(const InputDeviceIdentifier& deviceIdentifier,
const std::string& name) {
std::string path(getPath(deviceIdentifier, name, InputDeviceConfigurationFileType::KEY_LAYOUT)); //5、getPath 函数获取路径
if (path.empty()) {
return NAME_NOT_FOUND;
}
base::Result> ret = KeyLayoutMap::load(path);
if (ret.ok()) {
keyLayoutMap = *ret;
keyLayoutFile = path;
return OK;
}
。。。
}
//6、getPath 获取路径函数
static std::string getPath(const InputDeviceIdentifier& deviceIdentifier, const std::string& name,
InputDeviceConfigurationFileType type) {
return name.empty()
? getInputDeviceConfigurationFilePathByDeviceIdentifier(deviceIdentifier, type) //7、里面包含目录遍历
: getInputDeviceConfigurationFilePathByName(name, type); //8、具体目录的查找
}
//9、无论是目录遍历函数还是具体目录查找函数都是在 InputDevice 里面实现的,稍后追踪
//10、上面load函数没找到kl的情况,调用的方法
bool KeyMap::probeKeyMap(const InputDeviceIdentifier& deviceIdentifier,
const std::string& keyMapName) {
if (!haveKeyLayout()) {
loadKeyLayout(deviceIdentifier, keyMapName); //11、最后还是调用了getPath,正常情况就是它了!
}
if (!haveKeyCharacterMap()) {
loadKeyCharacterMap(deviceIdentifier, keyMapName);
}
return isComplete();
}
接上面9的点,继续往下追踪 InputDevice.cpp
//1、目录遍历函数
std::string getInputDeviceConfigurationFilePathByDeviceIdentifier(
const InputDeviceIdentifier& deviceIdentifier, InputDeviceConfigurationFileType type,
const char* suffix) {
if (deviceIdentifier.vendor !=0 && deviceIdentifier.product != 0) {
if (deviceIdentifier.version != 0) {
// Try vendor product version.
//2、看看看这里:Vendor_X_Product_X_Version_X的查看
//3、getInputDeviceConfigurationFilePathByName 具体目录的查找后续分析
std::string versionPath =
getInputDeviceConfigurationFilePathByName(StringPrintf("Vendor_%04x_Product_%"
"04x_Version_%04x%s",
deviceIdentifier.vendor,
deviceIdentifier.product,
deviceIdentifier.version,
suffix),
type);
if (!versionPath.empty()) { //4、Version找到了就返回
return versionPath;
}
}
// Try vendor product.
//5、只找Vendor和Product的情况
std::string productPath =
getInputDeviceConfigurationFilePathByName(StringPrintf("Vendor_%04x_Product_%04x%s",
deviceIdentifier.vendor,
deviceIdentifier.product,
suffix),
type);
if (!productPath.empty()) {
return productPath;//6、Version找到了就返回
}
}
// Try device name.
//7、Version和Vendor都找不到,就找节点命名名称
return getInputDeviceConfigurationFilePathByName(deviceIdentifier.getCanonicalName() + suffix,
type);
}
//8、具体目录的查找函数
std::string getInputDeviceConfigurationFilePathByName(
const std::string& name, InputDeviceConfigurationFileType type) {
// Search system repository.
std::string path;
// Treblized input device config files will be located /product/usr, /system_ext/usr,
// /odm/usr or /vendor/usr.
// These files may also be in the com.android.input.config APEX.
//9、看看看这里,根目录的遍历
const char* rootsForPartition[]{
"/product",
"/system_ext",
"/odm",
"/vendor",
"/apex/com.android.input.config/etc",
getenv("ANDROID_ROOT"), //10、这个是 system目录
};
for (size_t i = 0; i < size(rootsForPartition); i++) {
if (rootsForPartition[i] == nullptr) {
continue;
}
path = rootsForPartition[i];//11、遍历根目录
path += "/usr/";//11、遍历根目录+usr
appendInputDeviceConfigurationFileRelativePath(path, name, type);// 12、拼接path路径:path + name Vendor_xxx那些,type .文件后缀
。。。
return path;//
}
}
// Search user repository.
// TODO Should only look here if not in safe mode.//13、如果系统目录找不到,就用date下面的,这里也说了会不太安全
path = "";
char *androidData = getenv("ANDROID_DATA"); //14、根目录data
if (androidData != nullptr) {
path += androidData;
}
path += "/system/devices/"; //14、目录data/system/devices/
appendInputDeviceConfigurationFileRelativePath(path, name, type);// 15、拼接path路径
。。。
return path;
}
// 16、拼接path路径函数
static void appendInputDeviceConfigurationFileRelativePath(std::string& path,
const std::string& name, InputDeviceConfigurationFileType type) {
//17、path 根目录 + CONFIGURATION_FILE_DIR 文件类型 + 文件名称 + 后缀
path += CONFIGURATION_FILE_DIR[static_cast(type)];
path += name;
path += CONFIGURATION_FILE_EXTENSION[static_cast(type)];
}
// 18、文件类型,不用的文件类型是放在不同的目录下的
static const char* CONFIGURATION_FILE_DIR[] = {
"idc/", //idc文件目录
"keylayout/", //kl文件目录
"keychars/", //kcm文件目录
};
// 19、后缀
static const char* CONFIGURATION_FILE_EXTENSION[] = {
".idc",
".kl",
".kcm",
};
到这里基本代码是已经分析完成了,不清楚的可以自己看下具体代码了。代码挺多估计没几个人会仔细看!
按键相关的代码已上传,有兴趣的可以下载查看:
https://download.csdn.net/download/wenzhi20102321/88366484
直接修改设备当前选择的kl文件名称,或者cp一份kl文件到优先级高的目录,需要重启才能生效。
dumpsys input 查看某类按键键值当前使用的kl文件。
比如上面 dumpsys input 的event1的信息:
4: mtk-kpd //(1)按键事件的节点命名名称
Classes: 0x00000001
Path: /dev/input/event1 //(1)按键事件的节点位置,这个才是主要的,名称可以不看,但是节点必须找对
Identifier: bus=0x0019, vendor=0x2454, product=0x6500, version=0x0010 //(3)各版本号,寻找kl使用到
KeyLayoutFile: /system/usr/keylayout/mtk-kpd.kl //(4)实际起作用的kl文件
获取到有用的信息:
event1 按键,节点在底层的名称是 mtk-kpd
event1 按键的版本信息 Identifier:vendor=0x2454, product=0x6500, version=0x0010
event1 按键的实际起作用的kl文件目录 KeyLayoutFile:/system/usr/keylayout/mtk-kpd.kl
把 mtk-kpd.kl复制到 /product/usr/keylayout/
重启后,dumpsys input ,查看event1 的信息可以看到,实际起作用的kl文件 :
KeyLayoutFile: /product/usr/keylayout/mtk-kpd.kl
其他路径我就不一一测试了,有兴趣的可以尝试。
需要注意的是,kl遍历的文件目录 除了 /system/usr/keylayout/这个目录是系统生成的,
其他所有的目录我看了一下都是不存在的,除非你手动创建或者复制文件的时候创建。
所以未特别适配kl文件的系统,kl文件基本都是在 /system/usr/keylayout/ 这个目录,并且里面是有非常多的没啥作用的kl文件。
保存 /product/usr/keylayout/mtk-kpd.kl 的情况下,复制一个version文件到当前目录下
Version文件: Vendor_0001_Product_0001_Version_0100.kl
重启后,dumpsys input ,查看event1 的信息可以看到,实际起作用的kl文件 :
KeyLayoutFile: /product/usr/keylayout/Vendor_0001_Product_0001_Version_0100.kl
到这里可以看到,无论是修改文件路径还是文件名称都是有作用的。
并不是所有的 Version 都有数值的,这种情况如果添加 Vendor_0000_Product_0000_Version_0000.kl
或者 Vendor_0_Product_0_Version_0.kl 那么会选择优先选择这个文件吗?
是不会选择Version_0000/0的,这个我是有测试过的。估计这里0只是没到的检测到Version的默认显示,并不是真正的字符串。
其实 Generic.kl 是最后无奈的选择!
优先从所有目录下查看 Version文件和节点名称文件kl,如果没有找到最后在所有目录找一遍 Generic.kl 文件。
如果没有Version文件和节点名称文件kl的情况,
高优先级目录有 Generic.kl 文件就直接使用这个文件,不用再去 /system/usr/keylayout/ 查找。
上面都是在命令行查看实际使用的kl文件,但是想要测试是否真实准确,其实可以修改kl文件里面的内容确认。
修改:/system/usr/keylayout/mtk-kpd.kl
key 115 VOLUME_UP //音量加
key 114 VOLUME_UP //音量减,修改成音量加功能字符串
实现修改的方式可以pull文件后再push进去或者使用busybox vi XXX 功能,都是需要root权限的!
这里只是测试验证功能,实际没啥这样改的场景哈。
修改后重启设备测试效果,查看是否符合预期,复制到其他文件尝试。
不同的Android系统选择可能不一样,这里以Android13上的代码进行举例:
"/product/usr/keylayout/name.kl",
"/system_ext/usr/keylayout/name.kl",
"/odm/usr/keylayout/name.kl",
"/vendor/usr/keylayout/name.kl",
"/apex/com.android.input.config/etc/usr/keylayout/name.kl",
"/system/usr/keylayout/name.kl"
"/data/system/devices/keylayout/name.kl"
name.kl 名称的优先级:
Vendor_4x_Product_4x_Version_4x.kl
Vendor_4x_Product_4x.kl
eventName.kl //节点在底层的命名名称
Generic.kl
名称name的优先级选择的代码在
InputDevice.getInputDeviceConfigurationFilePathByDeviceIdentifier函数中体现;
目录path优先级选择的代码在
InputDevice.getInputDeviceConfigurationFilePathByName函数中体现。
优先从所有目录下查看 Version文件和节点名称文件kl,
如果没有找到最后在所有目录找一遍 Generic.kl 文件。
定义eventX节点的名称是在内核代码中决定的,具体选择哪个kl文件的逻辑是系统framework中。
并且可以在framework中进行修改kl文件选择的顺序。
这些都是Android系统的流程,正常情况有个了解就行了,一般不需要修改,
但是如果真的出现问题或有相关修改需求还是要分析适配的。