Android中外接键盘的检测

今天来了一个问题:软键盘没法弹出。分析后是由于系统判断当前有外接硬键盘,就会隐藏软键盘。但实际状况并非这么简单,该问题只有在特定条件下偶现,具体分析过程就不说了,就是软硬键盘支持上的逻辑问题。借着这个机会整理一下键盘检测的过程。java

Configuration

Android系统中经过读取Configuration中keyboard的值来判断是否存在外接键盘。Configuration中关于键盘类型的定义以下,ide

public static final int KEYBOARD_UNDEFINED = 0; // 未定义的键盘
    public static final int KEYBOARD_NOKEYS = 1; // 无键键盘,没有外接键盘时为该类型
    public static final int KEYBOARD_QWERTY = 2; // 标准外接键盘
    public static final int KEYBOARD_12KEY = 3; // 12键小键盘

在最多见的状况下,外接键盘未链接时keyboard的值为KEYBOARD_NOKEYS,当检测到键盘链接后会将keyboard的值更新为KEYBOARD_QWERTY 。应用就能够根据keyboard的值来判断是否存在外接键盘,InputMethodService.java中有相似的判断代码。ui

// 软件盘是否能够显示
    public boolean onEvaluateInputViewShown() {
        Configuration config = getResources().getConfiguration();
        return config.keyboard == Configuration.KEYBOARD_NOKEYS
                || config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES;
    }

如今的问题就转向Configuration的keyboard是如何更新的。在WindowManagerService.java中,应用启动时会更新Configuration,相关代码以下。this

boolean computeScreenConfigurationLocked(Configuration config) {
        ......
        if (config != null) {
            // Update the configuration based on available input devices, lid switch,
            // and platform configuration.
            config.touchscreen = Configuration.TOUCHSCREEN_NOTOUCH;
            // 默认值为KEYBOARD_NOKEYS
            config.keyboard = Configuration.KEYBOARD_NOKEYS;
            config.navigation = Configuration.NAVIGATION_NONAV;
            
            int keyboardPresence = 0;
            int navigationPresence = 0;
            final InputDevice[] devices = mInputManager.getInputDevices();
            final int len = devices.length;
            // 遍历输入设备
            for (int i = 0; i < len; i++) {
                InputDevice device = devices[i];
                // 若是不是虚拟输入设备,会根据输入设备的flags来更新Configuration
                if (!device.isVirtual()) {
                    ......
                    // 若是输入设备的键盘类型为KEYBOARD_TYPE_ALPHABETIC,则将keyboard设置为KEYBOARD_QWERTY
                    if (device.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
                        config.keyboard = Configuration.KEYBOARD_QWERTY;
                        keyboardPresence |= presenceFlag;
                    }
                }
            }
            ......
            // Determine whether a hard keyboard is available and enabled.
            boolean hardKeyboardAvailable = config.keyboard != Configuration.KEYBOARD_NOKEYS;
            // 更新硬件键盘状态
            if (hardKeyboardAvailable != mHardKeyboardAvailable) {
                mHardKeyboardAvailable = hardKeyboardAvailable;
                mH.removeMessages(H.REPORT_HARD_KEYBOARD_STATUS_CHANGE);
                mH.sendEmptyMessage(H.REPORT_HARD_KEYBOARD_STATUS_CHANGE);
            }
            // 若是Setting中SHOW_IME_WITH_HARD_KEYBOARD被设置,将keyboard设置为KEYBOARD_NOKEYS,让软件盘能够显示
            if (mShowImeWithHardKeyboard) {
                config.keyboard = Configuration.KEYBOARD_NOKEYS;
            }
            ......
        }

影响Configuration中keyboard的值有,lua

  • 默认值为KEYBOARD_NOKEYS,表示没有外接键盘。
  • 当输入设备为KEYBOARD_TYPE_ALPHABETIC时,更新为KEYBOARD_QWERTY,一个标准键盘。
  • 当Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD为1时,设置为KEYBOARD_NOKEYS,目的是让软键盘能够显示。

inputflinger

接下来须要关注输入设备时什么时候被设置KEYBOARD_TYPE_ALPHABETIC的。搜索代码能够看到,这个flag实在native代码中设置的,代码在inputflinger/InputReader.cpp中。native和java使用了同必定义值,若是修改定义时须要注意同时修改。native中的名字为AINPUT_KEYBOARD_TYPE_ALPHABETIC。code

InputDevice* InputReader::createDeviceLocked(int32_t deviceId, int32_t controllerNumber,
        const InputDeviceIdentifier& identifier, uint32_t classes) {
    InputDevice* device = new InputDevice(&mContext, deviceId, bumpGenerationLocked(),
            controllerNumber, identifier, classes);
    ......
    if (classes & INPUT_DEVICE_CLASS_ALPHAKEY) {
        keyboardType = AINPUT_KEYBOARD_TYPE_ALPHABETIC;
    }
    ......
    return device;
}

InputReader在增长设备时,根据classes的flag来设置键盘类型。这个flag又是在EventHub.cpp中设置的。orm

status_t EventHub::openDeviceLocked(const char *devicePath) {
    ......
    // Configure the keyboard, gamepad or virtual keyboard.
    if (device->classes & INPUT_DEVICE_CLASS_KEYBOARD) { 
        // 'Q' key support = cheap test of whether this is an alpha-capable kbd
        if (hasKeycodeLocked(device, AKEYCODE_Q)) {
            device->classes |= INPUT_DEVICE_CLASS_ALPHAKEY;
        }
    ......
}

看到这里就比较明确了,在EventHub加载设备时,若是输入设备为键盘,而且带有'Q'键,就认为这是一个标准的外接键盘。但为什么判断'Q'键还不是很清楚。xml

keylayout

上面说道经过'Q'键来判断是否为外接键盘,这个'Q'键是Android的键值,键值是否存在是经过一个keylayout文件决定的。kl文件存储在目标系统的/system/usr/keylayout/下,系统能够有多个kl文件,根据设备的ID来命名。当系统加载键盘设备时,就会根据设备的Vendor ID和Product ID在/system/usr/keylayout/下寻找kl文件。例如一个kl文件名为”Vendor_0c45_Product_1109.kl“,代表设备的Vendor ID为0c45,Product ID为1109。一个kl的内容示例以下,接口

key 1     BACK
key 28    DPAD_CENTER
key 102   HOME

key 103   DPAD_UP
key 105   DPAD_LEFT
key 106   DPAD_RIGHT
key 108   DPAD_DOWN

key 113   VOLUME_MUTE
key 114   VOLUME_DOWN
key 115   VOLUME_UP

key 142   POWER

键值映射须要使用关键之”key“进行声明,以后跟着的数字为Linux驱动中的键值定义,再后面的字符串是Android中按键的名称。'Q'键是否存在彻底取决于kl文件中是否有映射,而不是实际物理键是否存在。kl文件的查找也是有一个规则的,其查找顺序以下,rem

/system/usr/keylayout/Vendor_XXXX_Product_XXXX_Version_XXXX.kl

/system/usr/keylayout/Vendor_XXXX_Product_XXXX.kl

/system/usr/keylayout/DEVICE_NAME.kl

/data/system/devices/keylayout/Vendor_XXXX_Product_XXXX_Version_XXXX.kl

/data/system/devices/keylayout/Vendor_XXXX_Product_XXXX.kl

/data/system/devices/keylayout/DEVICE_NAME.kl

/system/usr/keylayout/Generic.kl

/data/system/devices/keylayout/Generic.kl

同时支持软硬键盘

有了上面的知识,就能够给出同时支持软硬键盘的方案。

  • 修改源码逻辑,设置Configuration中keyboard的值为KEYBOARD_NOKEYS。这种Hack其实很差,破坏原生逻辑,缺少移植性。非要这样改的话,能够增长对设备的判断,只有特定的键盘设备设置为KEYBOARD_NOKEYS,减小反作用。
  • 修改keylayout,去掉'Q'键映射。有时kl文件写的不标准,为了通用把全部键的映射都写上了,实际硬件键却不多,咱们就是这种状况。应该按照真实硬件来编写kl文件。
  • 设置Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD为1。我认为这是最标准的修改方式,也很是方便。

关于第三个方案的修改方式有两种,一种是修改缺省的setting值,在文件frameworks/base/packages/SettingsProvider/res/values/defaults.xml中增长,

<integer name="def_show_ime_with_hard_keyboard">1</integer>

另外一种方式是在系统启动时在代码中经过接口进行设置。

Settings.Secure.putInt(context.getContentResolver(), Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 1);