增加一个物理按键导致外接耳机音量键和暂停键无法响应

前段时间开发了一个功能,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的大致流程

你可能感兴趣的:(笔记)