前段時間開發了一個功能,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系統相關的流程如下:
- EventHub::scanDirLocked掃描所有的設備節點,就是我們前面說的device 1,device 2…,/dev/input/event4就是設備節點的path
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"
- 遍歷調用EventHub::openDeviceLocked打開每一個設備節點,然後探索系統提供的input設備配置文件,這裏主要搜索的目錄和文件被定義在InputDevice.cpp中:即搜索的目錄爲system/usr/idc/,system/usr/keylayout/,system/usr/keychars/,/data/system/devices/idc/,/data/system/devices/keylayout/,/data/system/devices/keychars/,
文件爲這些目錄下後綴爲.idc,.kl,.kcm的文件
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/");
- 搜索到系統有這些文件之後會通過一個工具類Tokenizer來解析
大致流程就是這樣,我們來看看解析的流程,外接耳機的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的大致流程