Android10 (.kl)按键布局文件的解析过程分析

.kl文件

简述

  kl文件也就是keylayout文件,它的作用是将Linux scancode转换为Android keycode。scancode就是硬件直接扫描到的数字,而这些数字会通过这个kl文件对应到字符串,也就是keycode。
  设备可以拥有自己专属的kl文件,命名规则和idc文件一样,这里就不重复说了。另外系统提供了一个特殊的内置常规按键布局文件,名为 Generic.kl。当找不到专属的kl时候就会用Generic.kl

示例

  下面示例是Generic.kl中一些键值对类型。当然设备的专属kl文件并不需要包含下方的所有类型,只需要包含会用到的就可以了

/odm/usr/keylayout/Generic.kl
/vendor/usr/keylayout/Generic.kl
/system/usr/keylayout/Generic.kl
/data/system/devices/keylayout/Generic.kl
# 键盘
key 1     ESCAPE
key 2     1
key 3     2
key 12    MINUS
key 13    EQUALS
key 14    DEL

# 系统控件
key 114   VOLUME_DOWN
key 115   VOLUME_UP
key 116   POWER

#电容式按钮
key 139    MENU           VIRTUAL
key 172    HOME           VIRTUAL
key 158    BACK           VIRTUAL
key 217    SEARCH         VIRTUAL

#耳机插孔媒体控件
key 163   MEDIA_NEXT
key 165   MEDIA_PREVIOUS
key 226   HEADSETHOOK

#操纵杆
key 304   BUTTON_A
key 305   BUTTON_B
key 307   BUTTON_X
key 308   BUTTON_Y

# Keys defined by HID usages
key usage 0x0c006F BRIGHTNESS_UP
key usage 0x0c0070 BRIGHTNESS_DOWN

EventHub::openDeviceLocked

  这个过程和加载idc一样也是从openDeviceLocked开始的,调用loadKeyMapLocked() 加载Kl文件

status_t EventHub::openDeviceLocked(const char* devicePath) {
    char buffer[80];
   
    // devicePath = /dev/input/eventx
    int fd = open(devicePath, O_RDWR | O_CLOEXEC | O_NONBLOCK);
    InputDeviceIdentifier identifier;
    ...

    // Load the key map.
    // We need to do this for joysticks too because the key layout may specify axes.
    status_t keyMapStatus = NAME_NOT_FOUND;
    if (device->classes & (INPUT_DEVICE_CLASS_KEYBOARD | INPUT_DEVICE_CLASS_JOYSTICK)) {
        // Load the keymap for the device.
        keyMapStatus = loadKeyMapLocked(device);
    }
    ...
    return OK;
}

  loadKeyMapLocked就直接调用keyMap.load了

status_t EventHub::loadKeyMapLocked(Device* device) {
    return device->keyMap.load(device->identifier, device->configuration);
}

KeyMap::load

  还记得我们之前解析idc文件的时候有去解析"keyboard.layout"和"keyboard.characterMap"这两项吗,在这个函数开头我们就是判断在该设备的idc文件中有没有指定kl和kcm文件,如果有的话我们就用idc文件中指定的,如果没有我们再通过probeKeyMap() 去各个目录下查找
  查找的话我们会先通过设备标识符去找它专属的kl文件,找不到就去找Generic,虽然有个最后的选择虚拟键盘映射,不过并不会用到它,因为Generic.kl总是存在的

\frameworks\native\libs\input\Keyboard.cpp
status_t KeyMap::load(const InputDeviceIdentifier& deviceIdenfifier,
        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(deviceIdenfifier, keyLayoutName.c_str());
            if (status == NAME_NOT_FOUND) {
                ALOGE("Configuration for keyboard device '%s' requested keyboard layout '%s' but "
                        "it was not found.",
                        deviceIdenfifier.name.c_str(), keyLayoutName.string());
            }
        }

        String8 keyCharacterMapName;
        if (deviceConfiguration->tryGetProperty(String8("keyboard.characterMap"),
                keyCharacterMapName)) {
            status_t status = loadKeyCharacterMap(deviceIdenfifier, keyCharacterMapName.c_str());
            if (status == NAME_NOT_FOUND) {
                ALOGE("Configuration for keyboard device '%s' requested keyboard character "
                        "map '%s' but it was not found.",
                        deviceIdenfifier.name.c_str(), keyLayoutName.string());
            }
        }

        if (isComplete()) {
            return OK;
        }
    }

    // Try searching by device identifier.
    if (probeKeyMap(deviceIdenfifier, "")) {
        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, "Generic")) {
        return OK;
    }

    // Try the Virtual key map as a last resort.
    if (probeKeyMap(deviceIdenfifier, "Virtual")) {
        return OK;
    }

    // Give up!
    ALOGE("Could not determine key map for device '%s' and no default key maps were found!",
            deviceIdenfifier.name.c_str());
    return NAME_NOT_FOUND;
}

KeyMap::probeKeyMap

  接下来看一下probeKeyMap() 的实现,它会分别去加载kl和kcm文件

bool KeyMap::probeKeyMap(const InputDeviceIdentifier& deviceIdentifier,
        const std::string& keyMapName) {
    if (!haveKeyLayout()) {
        loadKeyLayout(deviceIdentifier, keyMapName);
    }
    if (!haveKeyCharacterMap()) {
        loadKeyCharacterMap(deviceIdentifier, keyMapName);
    }
    return isComplete();
}

  先来看下isComplete函数,kl文件和kcm文件都有了才返回true,看load函数,当isComplete返回true,就直接return了,因为kl 和 kcm文件都找到了。

inline bool isComplete() const {
        return haveKeyLayout() && haveKeyCharacterMap();
}

  这里我们只看kl文件的加载过程,kcm类似

status_t KeyMap::loadKeyLayout(const InputDeviceIdentifier& deviceIdentifier,
        const std::string& name) {
    std::string path(getPath(deviceIdentifier, name,
            INPUT_DEVICE_CONFIGURATION_FILE_TYPE_KEY_LAYOUT));
    if (path.empty()) {
        return NAME_NOT_FOUND;
    }

    status_t status = KeyLayoutMap::load(path, &keyLayoutMap);
    if (status) {
        return status;
    }

    keyLayoutFile = path;
    return OK;
}

KeyMap::getPath

  getPath()就是调用getInputDeviceConfigurationFilePathByDeviceIdentifier()或getInputDeviceConfigurationFilePathByName()去获取路径

std::string KeyMap::getPath(const InputDeviceIdentifier& deviceIdentifier,
        const std::string& name, InputDeviceConfigurationFileType type) {
    return name.empty()
            ? getInputDeviceConfigurationFilePathByDeviceIdentifier(deviceIdentifier, type)
            : getInputDeviceConfigurationFilePathByName(name, type);
}

getInputDeviceConfigurationFilePathByDeviceIdentifier

  kl文件命名有三种方式

Vendor_xxxx_Product_xxxx_Version_xxxx.kl
Vendor_xxxx_Product_xxxx.kl
device-name.kl

  xxxx分别对应的VID、PID和版本号,第一种命名方式不常用

注意:设备名称中除“0-9”、“a-z”、“A-Z”、“-”或“_”之外的所有字符将替换为“_”

  getInputDeviceConfigurationFilePathByDeviceIdentifier中有三个分支是按照三种文件名分别去寻找kl文件,找到了返回配置文件的路径,找不到返回空的String

\frameworks\native\libs\input\InputDevice.cpp
std::string getInputDeviceConfigurationFilePathByDeviceIdentifier(
        const InputDeviceIdentifier& deviceIdentifier,
        InputDeviceConfigurationFileType type) {
    if (deviceIdentifier.vendor !=0 && deviceIdentifier.product != 0) {
        if (deviceIdentifier.version != 0) {
            // Try vendor product version.
            std::string versionPath = getInputDeviceConfigurationFilePathByName(
                    StringPrintf("Vendor_%04x_Product_%04x_Version_%04x",
                            deviceIdentifier.vendor, deviceIdentifier.product,
                            deviceIdentifier.version),
                    type);
            if (!versionPath.empty()) {
                return versionPath;
            }
        }

        // Try vendor product.
        std::string productPath = getInputDeviceConfigurationFilePathByName(
                StringPrintf("Vendor_%04x_Product_%04x",
                        deviceIdentifier.vendor, deviceIdentifier.product),
                type);
        if (!productPath.empty()) {
            return productPath;
        }
    }

    // Try device name.
    return getInputDeviceConfigurationFilePathByName(deviceIdentifier.getCanonicalName(), type);
}

getInputDeviceConfigurationFilePathByName

  传入的参数是要找的文件名和要找的文件类型

std::string getInputDeviceConfigurationFilePathByName(
        const std::string& name, InputDeviceConfigurationFileType type) {
    // Search system repository.
    std::string path;

    // Treblized input device config files will be located /odm/usr or /vendor/usr.
    const char *rootsForPartition[] {"/odm", "/vendor", getenv("ANDROID_ROOT")};
    for (size_t i = 0; i < size(rootsForPartition); i++) {
        if (rootsForPartition[i] == nullptr) {
            continue;
        }
        path = rootsForPartition[i];
        path += "/usr/";
        appendInputDeviceConfigurationFileRelativePath(path, name, type);

        if (!access(path.c_str(), R_OK)) {
            return path;
        }
    }

    // Search user repository.
    // TODO Should only look here if not in safe mode.
    path = "";
    char *androidData = getenv("ANDROID_DATA");
    if (androidData != nullptr) {
        path += androidData;
    }
    path += "/system/devices/";
    appendInputDeviceConfigurationFileRelativePath(path, name, type);

    if (!access(path.c_str(), R_OK)) {
        return path;
    }

    // Not found.
    return "";
}

appendInputDeviceConfigurationFileRelativePath

  用传进来的名字和路径加入kl文件目录和后缀,组成完整的文件名
  然后在getInputDeviceConfigurationFilePathByName中判断该路径下是否存在这个kl文件
有返回路径,没有返回空string

static void appendInputDeviceConfigurationFileRelativePath(std::string& path,
        const std::string& name, InputDeviceConfigurationFileType type) {
    path += CONFIGURATION_FILE_DIR[type];
    path += name;
    path += CONFIGURATION_FILE_EXTENSION[type];
}
static const char* CONFIGURATION_FILE_DIR[] = {
        "idc/",
        "keylayout/",
        "keychars/",
};

static const char* CONFIGURATION_FILE_EXTENSION[] = {
        ".idc",
        ".kl",
        ".kcm",
};

  到这里我们已经完成了寻找kl文件的支线任务,回到接下来我们回到loadConfigurationLocked()中,找到kl就返回路径和文件名并调用 KeyLayoutMap::load()

KeyLayoutMap::load

  Tokenizer::open将filename对应的的fd映射到内存中,初始化了一个Tokenizer。若成功则new一个KeyLayoutMap,以该KeyLayoutMap和Tokenizer为参数构造一个Parser对kl文件进行解析。
  KeyLayoutMap和PropertyMap的结构大致相同,都拥有嵌套类Parser。但是Parser的parse解析方法不同。

\frameworks\native\libs\input\KeyLayoutMap.cpp
status_t KeyLayoutMap::load(const std::string& filename, sp<KeyLayoutMap>* outMap) {
    outMap->clear();

    Tokenizer* tokenizer;
    status_t status = Tokenizer::open(String8(filename.c_str()), &tokenizer);
    if (status) {
        ALOGE("Error %d opening key layout map file %s.", status, filename.c_str());
    } else {
        sp<KeyLayoutMap> map = new KeyLayoutMap();
        if (!map.get()) {
            ALOGE("Error allocating key layout map.");
            status = NO_MEMORY;
        } else {
            Parser parser(map.get(), tokenizer);
            status = parser.parse();
            if (!status) {
                *outMap = map;
            }
        }
        delete tokenizer;
    }
    return status;
}

KeyLayoutMap::Parser::parse

  跳过空白行和注释行,如果第一个Token为“key”,将Tokenizer内置指针移到下一个Token起始处,调用parseKey进行解析

status_t KeyLayoutMap::Parser::parse() {
    while (!mTokenizer->isEof()) {
 		// 跳过空格
        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 {
                ALOGE("%s: Expected keyword, got '%s'.", mTokenizer->getLocation().string(),
                        keywordToken.string());
                return BAD_VALUE;
            }

            mTokenizer->skipDelimiters(WHITESPACE);
            if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') {
                ALOGE("%s: Expected end of line or trailing comment, got '%s'.",
                        mTokenizer->getLocation().string(),
                        mTokenizer->peekRemainderOfLine().string());
                return BAD_VALUE;
            }
        }

        mTokenizer->nextLine();
    }
    return NO_ERROR;
}

KeyLayoutMap::Parser::parseKey

  parseKey函数首先会继续通过nextToken函数获取按键scancode,对于scancode为"usage"的情况修改mapUsage 为true,指针指向下一个Token起始处,如下例子,codeToken就等于0x0c006F

key usage 0x0c006F BRIGHTNESS_UP
key usage 0x0c0070 BRIGHTNESS_DOWN

  对于非"usage"的情况,就会直接拿到scan code通过strtol函数进行转换,接着获取按键名称keyCodeToken(如POWER),通过getKeyCodeByLabel() 将Linux的扫描码转为Android的键盘码
  获取特殊功能按键的flag值,最后,将一个key进行键码值,flags值的初始化,以code-key的形式添加到mKeysByUsageCode或mKeysByScanCode中。

status_t KeyLayoutMap::Parser::parseKey() {
	//获取该行中第二个空格,tab键或者回车和第一个空格,tab键或者回车之间的所有字符,
    //例:key 116   POWER中,实际作用就是获取scan code为116
    String8 codeToken = mTokenizer->nextToken(WHITESPACE);
    bool mapUsage = false;
    //scan code为usage的情况
    if (codeToken == "usage") {// Keys defined by HID usages
        mapUsage = true;
        mTokenizer->skipDelimiters(WHITESPACE);
        // 下一个Token
        // key usage 0x0c006F BRIGHTNESS_UP中的0x0c006F
        codeToken = mTokenizer->nextToken(WHITESPACE);
    }

    char* end;
    // 字符串转十进制数,code为扫描码
    int32_t code = int32_t(strtol(codeToken.string(), &end, 0));//’0’表示十进制
    // UsageCode or ScanCode
    KeyedVector<int32_t, Key>& map = mapUsage ? mMap->mKeysByUsageCode : mMap->mKeysByScanCode;

    mTokenizer->skipDelimiters(WHITESPACE);
    //获取key 116   POWER中的POWER
    String8 keyCodeToken = mTokenizer->nextToken(WHITESPACE);
    //scancdoe转keycode
    int32_t keyCode = getKeyCodeByLabel(keyCodeToken.string());

    uint32_t flags = 0;
    for (;;) {
        mTokenizer->skipDelimiters(WHITESPACE);
        if (mTokenizer->isEol() || mTokenizer->peekChar() == '#') break;

        String8 flagToken = mTokenizer->nextToken(WHITESPACE);
        //对于特殊功能按键,获取flag
        uint32_t flag = getKeyFlagByLabel(flagToken.string());

        flags |= flag;
    }
	//构造Key
    Key key;
    key.keyCode = keyCode;
    key.flags = flags;
    map.add(code, key);
    return NO_ERROR;
}
struct Key {
        int32_t keyCode;
        uint32_t flags;
    };

getKeyCodeByLabel

  这个函数最大的作用就是通过InputEventLabel.h的getKeyCodeByLabel函数完成scancode到keycode的映射关系。

  另外两个解析函数parseAxis和parseLed的规则和parseKey类似,仅仅是其函数内部调用InputEventLabel.h的函数不一样,如parseAxis内部调用getAxisByLabel,parseLed内部调用getLedByLabel,这两个函数和getKeyCodeByLabel实现也基本一致,也只有用到的宏不同,这里就不去看了。

\frameworks\native\include\input\InputEventLabels.h
static inline int32_t getKeyCodeByLabel(const char* label) {
    return int32_t(lookupValueByLabel(label, KEYCODES));
}

  继续调用lookupValueByLabel函数,传了一个重要参数KEYCODES,KEYCODES是一个InputEventLabel结构体数组

struct InputEventLabel {
    const char *literal; // 字符串
    int value;// keycode键值
};

static const InputEventLabel KEYCODES[] = {
	DEFINE_KEYCODE(UNKNOWN),
    DEFINE_KEYCODE(SOFT_LEFT),
	... 
    DEFINE_KEYCODE(NUM),
    DEFINE_KEYCODE(HEADSETHOOK),
    DEFINE_KEYCODE(FOCUS),   // *Camera* focus
    DEFINE_KEYCODE(PLUS),
    DEFINE_KEYCODE(MENU),
}

// 宏定义如下:
#define DEFINE_KEYCODE(key) { #key, AKEYCODE_##key }

  通过宏DEFINE_KEYCODE将kl文件中的按键名称生成对应的安卓键值,例如POWER通过DEFINE_KEYCODE就可以得到{“POWER”,AKEYCODE_POWER}的对应关系,"POWER"是按键名称,其按键值为AKEYCODE_POWER

lookupValueByLabel

  函数会遍历KEYCODES数组,通过C库函数strcmp查找literal是否包含在KEYCODES数组,返回值是InputEventLabel结构体的value,即根据"POWER"则返回AKEYCODE_POWER

static int lookupValueByLabel(const char* literal, const InputEventLabel *list) {
    while (list->literal) {
        if (strcmp(literal, list->literal) == 0) {
            return list->value;
        }
        list++;
    }
    return list->value;
}

  AKEYCODE_POWER的具体数值定义在keycodes.h中

\frameworks\native\include\android\keycodes.h
/**
 * Key codes.
 */
enum {
    AKEYCODE_UNKNOWN         = 0,
    AKEYCODE_SOFT_LEFT       = 1,
    ... 
    /** Power key. */
    AKEYCODE_POWER           = 26,
    ...
}

  我们可以看到这里面定义了很多AKEYCODE_XX的枚举值,AKEYCODE_POWER对应的数值就是26
  最终getKeyCodeByLabel函数就会根据按键名称,返回对应的数值,这即是scancode和keycode的一一映射关系。

getKeyFlagByLabel

另外还有一些按键是这种格式:

key 476   F11               FUNCTION
key 477   F12               FUNCTION
key 478   1                 FUNCTION

  就会继续通过nextToken获取flagToken等于FUNCTION,再调用getKeyFlagByLabel函数获取flag,这个函数和前面getKeyCodeByLabel一样的作用,它会从FLAGS数组中寻找:

static inline uint32_t getKeyFlagByLabel(const char* label) {
    return uint32_t(lookupValueByLabel(label, FLAGS));
}
static const InputEventLabel FLAGS[] = {DEFINE_FLAG(VIRTUAL),
                                        DEFINE_FLAG(FUNCTION),
                                        DEFINE_FLAG(GESTURE),
                                        DEFINE_FLAG(WAKE),

                                        {nullptr, 0}};
#define DEFINE_FLAG(flag) { #flag, POLICY_FLAG_##flag }

  拿到FUNCTION展开宏DEFINE_FLAG得到POLICY_FLAG_FUNCTION,POLICY_FLAG_FUNCTION的值定义在Input.h,指示这是一个特殊功能的按键。

enum {
    ...
    // Indicates that the key is the special function modifier.
    POLICY_FLAG_FUNCTION = 0x00000004,
    ...
    }

到这里解析kl文件的过程就分析完啦,接下来我们看一下解析出来的键码值在哪里被应用吧

键码值的应用

  当kernel上报的输入事件是按键类型时,我们会去检查上报的keycode在kl中是否有被定义,再通过我们之前生成的scanCode和Android键盘码的对应关系去将Linux上报的scanCode转化为Android的键盘码
  我们从输入事件中获取linux的扫描码是在KeyboardInputMapper::process中,这个函数前面的调用流程如下,有兴趣可以看看
  InputReader::loopOnce -> InputReader::processEventsLocked ->
InputReader::processEventsForDeviceLocked -> InputDevice::process -> (mapper->process) -> KeyboardInputMapper::process

KeyboardInputMapper::process

  如果输入事件类型是EV_KEY,就去获取Linux的scanCode

\frameworks\native\services\inputflinger\InputReader.cpp
void KeyboardInputMapper::process(const RawEvent* rawEvent) {
    switch (rawEvent->type) {
    case EV_KEY: {
        int32_t scanCode = rawEvent->code;
        int32_t usageCode = mCurrentHidUsage;
        mCurrentHidUsage = 0;

        if (isKeyboardOrGamepadKey(scanCode)) {
            processKey(rawEvent->when, rawEvent->value != 0, scanCode, usageCode);
        }
        break;
    }
    ...
}

  然后调用isKeyboardOrGamepadKey来判断键盘扫描码是否正确,如果正确则调用processKey来进一步处理

bool KeyboardInputMapper::isKeyboardOrGamepadKey(int32_t scanCode) {
    return scanCode < BTN_MOUSE
        || scanCode >= KEY_OK //352 
        || (scanCode >= BTN_MISC && scanCode < BTN_MOUSE) // 272
        || (scanCode >= BTN_JOYSTICK && scanCode < BTN_DIGI);
}

KeyboardInputMapper::processKey

  调用EventHub的mapKey将扫描码转为键盘码,这个函数省略的部分是key事件的处理过程,主要是根据我们转换好的键盘码、newMetaState、按下的时间对事件进行处理,处理好通知Listener,将事件交给Dispatch线程,这里就不详细说明了

void KeyboardInputMapper::processKey(nsecs_t when, bool down, int32_t scanCode,
        int32_t usageCode) {
    int32_t keyCode;
    int32_t keyMetaState;
    uint32_t policyFlags;
	// 扫描码转为键盘码
    if (getEventHub()->mapKey(getDeviceId(), scanCode, usageCode, mMetaState,
                              &keyCode, &keyMetaState, &policyFlags)) {
        keyCode = AKEYCODE_UNKNOWN;
        keyMetaState = mMetaState;
        policyFlags = 0;
    }
	...
    NotifyKeyArgs args(mContext->getNextSequenceNum(), when, getDeviceId(), mSource,
            getDisplayId(), policyFlags, down ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP,
            AKEY_EVENT_FLAG_FROM_SYSTEM, keyCode, scanCode, keyMetaState, downTime);
    getListener()->notifyKey(&args);
}

参考资料

Touch—load kl文件的过程
Android加载按键文件流程
Android6.0 按键kl文件加载过程分析
AndroidR Input子系统(5)解析“.kl“文件

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值