前段时间开发了一个功能,kaios设备上新增了一个物理按键,此按键用来进行快捷拨号,可以添加一个号码,通过点击三次实现拨号,我实现此功能的策略是选择了一个系统不用的按键F12,通过替换的方式,将F12替换为新增按键Qd,具体实现可见新增物理按键处理-kaios,当时开发完成之后详细测试了设备的基础功能,以及按键,没发现问题,最近测试同事给报出来外接耳机无法响应音量键和暂停键了,这里记录下调查此问题的过程。
kaios系统和Android系统底层Input系统几乎一样,首先得知此问题第一反应就是外接耳机的三个按键没有发到上层,来到nsAppShell.cpp的如下关键方法打log,一般kaios上层没有收到按键事件的话大概率是在此方法中被reture了
void
KeyEventDispatcher::Dispatch()
{
LOG("KeyEventDispatcher::Dispatch...mDOMKeyCode = :%d,mDOMKeyNameIndex = : %d,mData.key.keyCode = :%d,mData.key.scanCode = :%d",mDOMKeyCode,mDOMKeyNameIndex,mData.key.keyCode,mData.key.scanCode);
if (!mDOMKeyCode && mDOMKeyNameIndex == KEY_NAME_INDEX_Unidentified) {
VERBOSE_LOG("Got unknown key event code. "
"type 0x%04x code 0x%04x value %d",
mData.action, mData.key.keyCode, IsKeyPress());
return;
}
if (mDOMKeyNameIndex == KEY_NAME_INDEX_Flip){
hal::NotifyFlipStateFromInputDevice(!IsKeyPress());
return;
}
if (IsKeyPress()) {
DispatchKeyDownEvent();
} else {
DispatchKeyUpEvent();
}
}
如下log是点击耳机的暂停键打的,很明显看到,耳机暂停键的keycode没有拿到,mData.key.keyCode = :0,为0,但是scanCode是有的mData.key.scanCode = :226,说明此问题根本原因是通过scanCode没有获取到对应的keyCode
03-16 16:26:56.844 340 340 I dongjiao: KeyEventDispatcher::Dispatch...mDOMKeyCode = :0,mDOMKeyNameIndex = : 0,mData.key.keyCode = :0,mData.key.scanCode = :226
03-16 16:26:56.844 340 340 I dongjiao: KeyEventDispatcher::Dispatch...mDOMKeyCode = :0,mDOMKeyNameIndex = : 0,mData.key.keyCode = :0,mData.key.scanCode = :226
奇怪的是,我添加的Qd按钮和外接耳机的按键没有一点关系,而且为何只有耳机的按键出问题,设备其他按键都是正常的?
首先还是调查为何没拿到keyCode
InputReader通过EventHub读取到设备节点的原始事件之后会通过InputReader进行加工处理,InputReader根据不同类型的InputMapper调用对应的process函数,外接耳机的事件属于KeyboardInputMapper,外接耳机的scanCode映射keyCode的函数就是其process中的mapKey
void KeyboardInputMapper::process(const RawEvent* rawEvent) {
switch (rawEvent->type) {
case EV_KEY: {
.....
if (getEventHub()->mapKey(getDeviceId(), scanCode, usageCode, &keyCode, &flags)) {
keyCode = AKEYCODE_UNKNOWN;
flags = 0;
}
....
break;
}
如果这里getEventHub()->mapKey返回false,则keyCode就被赋值为AKEYCODE_UNKNOWN为0,此函数传递了一个很重要的参数getDeviceId(),这个Id就是读取事件的设备节点Id,通过adb shell getevent可以看到
add device 1: /dev/input/event4
name: "msm8909-snd-card Headset Jack"
add device 2: /dev/input/event3
name: "msm8909-snd-card Button Jack"
add device 3: /dev/input/event1
name: "qpnp_pon"
could not get driver version for /dev/input/mice, Not a typewriter
add device 4: /dev/input/event0
name: "matrix_keypad"
add device 5: /dev/input/event2
name: "gpio-keys"
外接耳机的deviceId是2,即事件会从/dev/input/event3中监听到
接着去看EventHub的mapKey函数:
status_t EventHub::mapKey(int32_t deviceId, int32_t scanCode, int32_t usageCode,
int32_t* outKeycode, uint32_t* outFlags) const {
AutoMutex _l(mLock);
Device* device = getDeviceLocked(deviceId);
......
// Check the key layout next.
ALOGD("dongjiao...EventHub::mapKey = :%d,deviceId = :%d",device->keyMap.haveKeyLayout(),deviceId);
if (device->keyMap.haveKeyLayout()) {
if (!device->keyMap.keyLayoutMap->mapKey(
scanCode, usageCode, outKeycode, outFlags)) {
return NO_ERROR;
}
}
}
*outKeycode = 0;
*outFlags = 0;
return NAME_NOT_FOUND;
}
这里打log发现device->keyMap.haveKeyLayout()返回false,根本没有进到keyLayoutMap->mapKey函数中去映射keycode
haveKeyLayout这个函数实现在Keyboard.h
String8 keyLayoutFile;
inline bool haveKeyLayout() const {
return !keyLayoutFile.isEmpty();
}
返回keyLayoutFile这个string是否为空,那么接着就需要找到keyLayoutFile是在哪里赋值的,为何读取外接耳机的事件时keyLayoutFile为空
来到Keyboard.cpp中
status_t KeyMap::loadKeyLayout(const InputDeviceIdentifier& deviceIdentifier,
const String8& name) {
String8 path(getPath(deviceIdentifier, name,
INPUT_DEVICE_CONFIGURATION_FILE_TYPE_KEY_LAYOUT));
if (path.isEmpty()) {
return NAME_NOT_FOUND;
}
if (status) {
return status;
}
keyLayoutFile.setTo(path);
return OK;
}
我们可以看到,loadKeyLayout函数中给keyLayoutFile赋值的,这里有两种情况回直接return,path为空或者status不为0
每次设备开机时和Input系统相关的流程如下:
add device 1: /dev/input/event4
name: "msm8909-snd-card Headset Jack"
add device 2: /dev/input/event3
name: "msm8909-snd-card Button Jack"
add device 3: /dev/input/event1
name: "qpnp_pon"
could not get driver version for /dev/input/mice, Not a typewriter
add device 4: /dev/input/event0
name: "matrix_keypad"
add device 5: /dev/input/event2
name: "gpio-keys"
static const char* CONFIGURATION_FILE_DIR[] = {
"idc/",
"keylayout/",
"keychars/",
};
static const char* CONFIGURATION_FILE_EXTENSION[] = {
".idc",
".kl",
".kcm",
};
system/usr/和/data/system/device前缀是怎么来的?ANDROID_ROOT环境变量为system,ANDROID_DATA环境变量为data
path.setTo(getenv("ANDROID_ROOT"));
path.append("/usr/");
path.setTo(getenv("ANDROID_DATA"));
path.append("/system/devices/");
大致流程就是这样,我们来看看解析的流程,外接耳机的deviceId为2,input节点为/dev/input/event3,name为msm8909-snd-card Button Jack,
看看Keyboard.cpp的load函数:
status_t KeyMap::load(const InputDeviceIdentifier& deviceIdenfifier,
const PropertyMap* deviceConfiguration) {
.....
// Try searching by device identifier.
if (probeKeyMap(deviceIdenfifier, String8::empty())) {
return OK;
}
// Fall back on the Generic key map.
// TODO Apply some additional heuristics here to figure out what kind of
// generic key map to use (US English, etc.) for typical external keyboards.
if (probeKeyMap(deviceIdenfifier, String8("Generic"))) {
return OK;
}
// Try the Virtual key map as a last resort.
if (probeKeyMap(deviceIdenfifier, String8("Virtual"))) {
return OK;
}
// Give up!
ALOGE("dongjiao...Could not determine key map for device '%s' and no default key maps were found!",
deviceIdenfifier.name.string());
return NAME_NOT_FOUND;
}
这里会调用函数probeKeyMap来解析映射表,传递null字符串会查找设备节点定义的表,即msm8909-snd-card Button Jack,如果没找到,则传递Generic找通用的表,最后传递Virtual查找虚拟表,
外接耳机的msm8909-snd-card Button Jack的表系统并没有,但是Generic这张表是有的啊,怎么会返回NAME_NOT_FOUND?我们追踪查找Generic表的流程
调用probeKeyMap函数
bool KeyMap::probeKeyMap(const InputDeviceIdentifier& deviceIdentifier,
const String8& keyMapName) {
if (!haveKeyLayout()) {
ALOGE("dongjiao...probeKeyMap keyMapName = :%s",keyMapName.string());
loadKeyLayout(deviceIdentifier, keyMapName);
}
......
return isComplete();
}
进一步调用loadKeyLayout函数:
来到了我们前面分析的给keyLayoutFile赋值的函数了,
status_t KeyMap::loadKeyLayout(const InputDeviceIdentifier& deviceIdentifier,
const String8& name) {
String8 path(getPath(deviceIdentifier, name,
INPUT_DEVICE_CONFIGURATION_FILE_TYPE_KEY_LAYOUT));
if (path.isEmpty()) {
ALOGE("dongjiao...path.isEmpty() = :%s,deviceIdentifier.name = %s:",path.string(),deviceIdentifier.name.string());
return NAME_NOT_FOUND;
}
ALOGE("dongjiao...path.isNotEmpty() = :%s,deviceIdentifier.name = :%s",path.string(),deviceIdentifier.name.string());
status_t status = KeyLayoutMap::load(path, &keyLayoutMap);
if (status) {
return status;
}
ALOGE("dongjiao...path = :%s,name = :%s",path.string(),name.string());
keyLayoutFile.setTo(path);
return OK;
}
先通过getpath获取要解析的文件路径,传递的参数name为Generic,文件类型为INPUT_DEVICE_CONFIGURATION_FILE_TYPE_KEY_LAYOUT,代表查找后缀为kl的文件,所以这里查找的是Generic.kl
enum InputDeviceConfigurationFileType {
INPUT_DEVICE_CONFIGURATION_FILE_TYPE_CONFIGURATION = 0, /* .idc file */
INPUT_DEVICE_CONFIGURATION_FILE_TYPE_KEY_LAYOUT = 1, /* .kl file */
INPUT_DEVICE_CONFIGURATION_FILE_TYPE_KEY_CHARACTER_MAP = 2, /* .kcm file */
};
在loadKeyLayout函数中打印log,Generic.kl并不为空,但最终keyLayoutFile却为空,所以一定是status非0
03-16 10:50:59.994 348 1094 E Keyboard: dongjiao...probeKeyMap keyMapName = :Generic
03-16 10:50:59.994 348 1094 D InputDevice: dongjiao...Probing for system provided input device configuration file: path='/system/usr/keylayout/Generic.kl'
03-16 10:50:59.994 348 1094 D InputDevice: dongjiao...Found
03-16 10:50:59.994 348 1094 E Keyboard: dongjiao...path.isNotEmpty() = :/system/usr/keylayout/Generic.kl,deviceIdentifier.name = :Virtual
KeyLayoutMap::load会解析Generic.kl
status_t KeyLayoutMap::load(const String8& filename, sp<KeyLayoutMap>* outMap) {
outMap->clear();
Tokenizer* tokenizer;
status_t status = Tokenizer::open(filename, &tokenizer);
if (status) {
ALOGE("dongjiao....Error %d opening key layout map file %s.", status, filename.string());
} else {
sp<KeyLayoutMap> map = new KeyLayoutMap();
.....
Parser parser(map.get(), tokenizer);
status = parser.parse();
......
}
return status;
}
首先打开Generic.kl,接着调用parser进一步解析Generic.kl
static const char* WHITESPACE = " \t\r";
status_t KeyLayoutMap::Parser::parse() {
while (!mTokenizer->isEof()) {
//遇见" \t\r"就跳过,
mTokenizer->skipDelimiters(WHITESPACE);
//不是结尾,并且不是“#”
if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') {
String8 keywordToken = mTokenizer->nextToken(WHITESPACE);
if (keywordToken == "key") {
mTokenizer->skipDelimiters(WHITESPACE);
status_t status = parseKey();
if (status) return status;
} else if (keywordToken == "axis") {
mTokenizer->skipDelimiters(WHITESPACE);
status_t status = parseAxis();
if (status) return status;
} else if (keywordToken == "led") {
mTokenizer->skipDelimiters(WHITESPACE);
status_t status = parseLed();
if (status) return status;
} else {
keywordToken.string());
return BAD_VALUE;
}
mTokenizer->skipDelimiters(WHITESPACE);
if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') {
return BAD_VALUE;
}
}
//下一行
mTokenizer->nextLine();
}
return NO_ERROR;
}
这个函数其实就是定义了解析文件的规则,通过循环解析Generic.kl文件,
遇见" \t\r"就跳过,遇到“key”调用parseKey解析,遇到“axis”调用parseAxis解析,遇到“led”调用parseLed解析,遇到“#”跳过解析下一行
所以解析Generic.kl核心方法其实是parse***,我们看看parseKey
status_t KeyLayoutMap::Parser::parseKey() {
......
mTokenizer->skipDelimiters(WHITESPACE);
String8 keyCodeToken = mTokenizer->nextToken(WHITESPACE);
int32_t keyCode = getKeyCodeByLabel(keyCodeToken.string());
if (!keyCode) {
ALOGE("%s: Expected key code label, got '%s'.", mTokenizer->getLocation().string(),
keyCodeToken.string());
return BAD_VALUE;
}
......
}
mTokenizer->getLocation().string()是映射表所在的路径,keyCodeToken.string()是映射表中的key的名字,当调查到此函数时,我已经知道出问题的原因了,在解析Generic.kl文件时,会将所有的合法的Key的名字解析出来,然后通过getKeyCodeByLabel获取keyCode,getKeyCodeByLabel函数其实就是从KeycodeLabel.h中的KEYCODES[]数组中根据key的名字获取keyCode
static const KeycodeLabel KEYCODES[] = {
{ "SOFT_LEFT", 1 },
{ "SOFT_RIGHT", 2 },
{ "HOME", 3 },
{ "BACK", 4 },
{ "CALL", 5 },
{ "ENDCALL", 6 },
{ "0", 7 },
{ "1", 8 },
{ "2", 9 },
{ "3", 10 },
{ "4", 11 },
{ "5", 12 },
{ "6", 13 },
{ "7", 14 },
{ "8", 15 },
{ "9", 16 },
...
{ "BUTTON_R1", 103 },
{ "BUTTON_L2", 104 },
{ "BUTTON_R2", 105 },
{ "BUTTON_THUMBL", 106 },
{ "BUTTON_THUMBR", 107 },
...
{ "FORWARD", 125 },
{ "MEDIA_PLAY", 126 },
{ "MEDIA_PAUSE", 127 },
{ "MEDIA_CLOSE", 128 },
{ "MEDIA_EJECT", 129 },
{ "MEDIA_RECORD", 130 },
{ "F1", 131 },
{ "F2", 132 },
{ "F3", 133 },
{ "F4", 134 },
{ "F5", 135 },
{ "F6", 136 },
{ "F7", 137 },
{ "F8", 138 },
{ "F9", 139 },
{ "F10", 140 },
{ "F11", 141 },
{ "QD_CALL", 142 },
...
}
我在开发快捷拨号这个功能的时候是将Qd_CALL这个新增按键通过替换的方式加入的,而被替换的就是F12按键,当拿着F12的名字向KEYCODES[]数组查询对应Keycode时就找不到,所以返回false,接着返回BAD_VALUE,BAD_VALUE是一个非0的数,最终返回到loadKeyLayout函数中,status为非0,就不会给keyLayoutFile设置path,所以keyLayoutFile就为空了
status_t KeyMap::loadKeyLayout(const InputDeviceIdentifier& deviceIdentifier,
const String8& name) {
String8 path(getPath(deviceIdentifier, name,
INPUT_DEVICE_CONFIGURATION_FILE_TYPE_KEY_LAYOUT));
if (path.isEmpty()) {
return NAME_NOT_FOUND;
}
status_t status = KeyLayoutMap::load(path, &keyLayoutMap);
if (status) {
return status;
}
keyLayoutFile.setTo(path);
return OK;
}
这里的keyLayoutFile为空是指,设备Id为2的,设备节点为 /dev/input/event3的,name为msm8909-snd-card Button Jack下的keyLayoutFile为空,所以就无法监听到此设备节点的事件,其他设备节点都是正常的,这就回答了我开头的问题,为什么其他按键是正常的,因为其他按键不在/dev/input/event3节点下监听,所以导致了最开始我这个功能没有测出来问题
解决方案很简单,就是将已经被替换的F12从Generic.kl文件中删除,这样就不会解析失败了
这篇文章主要通过调查问题,分析了设备开机以后扫描所有设备节点,并加载系统提供的input设备配置文件,并通过scancode映射keycode的大致流程