增加一個物理按鍵導致外接耳機音量鍵和暫停鍵無法響應

前段時間開發了一個功能,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系統相關的流程如下:

  1. 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"
  1. 遍歷調用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/");
  1. 搜索到系統有這些文件之後會通過一個工具類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的大致流程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章