基于Android 8.0源码,分析InputManagerService给Physical Keyboard 设置Keyboard Layout(KL)的具体过程。
| 1 | frameworks/native/services/inputflinger/ | 
Gityuan博客有详细分析Input系统,从该博客可以学习到Input模块的工作原理, 以及主要组成:
当Physical Keyboard(后面会直接用PK代替) 通过USB与Android设备连接时,首先触发的是硬件驱动,UsbHostManager 识别PK并发出USB_DEVICE_ATTACHED广播, EventHub通过InputReader线程会循环读取消息并调用getEvents()读取输入事件。
调用的流程是:1
EventHub::getEvents -> EventHub::scanDevicesLocked -> EventHub::scanDirLocked -> EventHub::openDeviceLocked
查看openDeviceLocked方法,可以知道此方法首先打开devicePath,然后new Device,调用LoadKeyMapLocked()来给PK load相应的.kl和.kcm, 所有的.kl文件和.kcm文件都放在/framework/base/data/keyboards/下面。我们可以看到有许多Vendor_XXXX_PRODUCT_XXXX命名的.kl和.kcm file,在loadKeymap时就是根据Device的vendor, product值来查找有没有相对应的Vendor_XXXX_PRODUCT_XXXX文件。多数情况下都不会有与PK对应的.kl和.kcm文件,这时Input系统会load默认的Generic.kl和Generic.kcm给该PK (Generic用的是qwerty layout)。
这也就是为什么有一些法语键盘或者德语键盘链接Android设备之后还是英语的layout的原因。
详细的调用流程可以看源码或者是这篇博文https://blog.csdn.net/kc58236582/.
以上简单介绍了Input系统处理PK连接事件的过程和为其设置.kl和.kcm文件的过程。接下来我将详细介绍一下重新给PK设置Keyboard lauout的过程还有重载新的layout的过程。
如上所述,当我们用到非英语键盘时,layout还会加载成qwert的,面对这个问题Input系统已经提供了相应的function供开发者调用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45/frameworks/base/services/java/com/android/server/input/InputManagerService.java
    @Override
    public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
            InputMethodInfo imeInfo, InputMethodSubtype imeSubtype,
            String keyboardLayoutDescriptor) {
        if (!checkCallingPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT,
                "setKeyboardLayoutForInputDevice()")) {
            throw new SecurityException("Requires SET_KEYBOARD_LAYOUT permission");
        }
        if (keyboardLayoutDescriptor == null) {
            throw new IllegalArgumentException("keyboardLayoutDescriptor must not be null");
        }
        if (imeInfo == null) {
            throw new IllegalArgumentException("imeInfo must not be null");
        }
        InputMethodSubtypeHandle handle = new InputMethodSubtypeHandle(imeInfo, imeSubtype);
        setKeyboardLayoutForInputDeviceInner(identifier, handle, keyboardLayoutDescriptor);
    }
    private void setKeyboardLayoutForInputDeviceInner(InputDeviceIdentifier identifier,
            InputMethodSubtypeHandle imeHandle, String keyboardLayoutDescriptor) {
        String key = getLayoutDescriptor(identifier);
        synchronized (mDataStore) {
            try {
                if (mDataStore.setKeyboardLayout(key, imeHandle, keyboardLayoutDescriptor)) { (见2.2.1)
                    if (DEBUG) {
                        Slog.d(TAG, "Set keyboard layout " + keyboardLayoutDescriptor +
                                " for subtype " + imeHandle + " and device " + identifier +
                                " using key " + key);
                    }
                    if (imeHandle.equals(mCurrentImeHandle)) {
                        if (DEBUG) {
                            Slog.d(TAG, "Layout for current subtype changed, switching layout");
                        }
                        SomeArgs args = SomeArgs.obtain();
                        args.arg1 = identifier;
                        args.arg2 = imeHandle;
                        mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, args).sendToTarget();(见2.2.3)
                    }
                    mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
                }
            } finally {
                mDataStore.saveIfNeeded();
            }
        }
    }
此方法被用在Settings/inputmethod里.
下面我们分析Input系统是如何在底层运作的。
这个package维护的是一个接收“QUERY_KEYBOARD_LAYOUTS”的BroadcastReceiver。1
2
3
4
5
6
7
8
9/src/com/android/inputdevices/InputDeviceReceiver.java
        <receiver android:name=".InputDeviceReceiver"
                android:label="@string/keyboard_layouts_label">
            <intent-filter>
                <action android:name="android.hardware.input.action.QUERY_KEYBOARD_LAYOUTS" />
            </intent-filter>
            <meta-data android:name="android.hardware.input.metadata.KEYBOARD_LAYOUTS"
                    android:resource="@xml/keyboard_layouts" />
        </receiver>
这里还存放了重要的/res/xml/keyboardlayouts.xml 和 /res/raw/…kcm, /res/raw/下存放了所有语言的.kcm files. xml文件是map所有.kcm文件用的。1
2
3
4
5
6
7
8
9
10<?xml version="1.0" encoding="utf-8"?>
<keyboard-layouts xmlns:android="http://schemas.android.com/apk/res/android">
    <keyboard-layout android:name="keyboard_layout_english_uk"
            android:label="@string/keyboard_layout_english_uk_label"
            android:keyboardLayout="@raw/keyboard_layout_english_uk" />
...
    <keyboard-layout android:name="keyboard_layout_latvian"
            android:label="@string/keyboard_layout_latvian"
            android:keyboardLayout="@raw/keyboard_layout_latvian_qwerty" />
</keyboard-layouts>
此章节将介绍IMS中重要的方法和相应的Class。
IMS通过PersistentDataStore来存储所有的Input devices的信息。在/data/system/input-manager-state.xml中。
通过adb可以查看该文件:1
2
3
4
5
6
7
8
9
10
11/data/system # cat input-manager-state.xml                                                                                                                      <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<input-manager-state>
    <input-devices>
        <input-device descriptor="vendor:16700,product:8467">
            <keyboard-layout descriptor="com.android.inputdevices/com.android.inputdevices.InputDeviceReceiver/keyboard_layout_french" input-method-id="com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME" input-method-subtype-id="843948332" current="true" />
        </input-device>
        <input-device descriptor="vendor:1266,product:1027">
            <keyboard-layout descriptor="com.android.inputdevices/com.android.inputdevices.InputDeviceReceiver/keyboard_layout_french" input-method-id="com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME" input-method-subtype-id="-921088104" current="true" />
        </input-device>
    </input-devices>
</input-manager-state>
我们可以看到其中com.android.inputdevices.InputDeviceReceiver/keyboard_layout_french就是对应着2.1.2中的Keyboard-layout name.
IMS.mDataStore就是PersistentDataStore的实例,以上IMS +1391行code就是在set相应的layout值。
这个方法会被native层用到,之后的篇幅会介绍到。此方法主要功能就是遍历2.1.2中的keyboardlayouts.xml维护的所有的layouts。
还有另外一个方法是visitKeyboardLayout()会被Settings.getKeyboardLayout()用到,原理也是一样的,遍历xml找到相应的layout。
具体实现是如下:1
2
3
4
5
6
7
8
9
10
11private void visitAllKeyboardLayouts(KeyboardLayoutVisitor visitor) {
    final PackageManager pm = mContext.getPackageManager();
    Intent intent = new Intent(InputManager.ACTION_QUERY_KEYBOARD_LAYOUTS);
    for (ResolveInfo resolveInfo : pm.queryBroadcastReceivers(intent,
            PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE
                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE)) {
        final ActivityInfo activityInfo = resolveInfo.activityInfo;
        final int priority = resolveInfo.priority;
        visitKeyboardLayoutsInPackage(pm, activityInfo, null, priority, visitor);
    }
}
其中InputManager.ACTION_QUERY_KEYBOARD_LAYOUTS 就是2.1.1中的QUERY_KEYBOARD_LAYOUTS。
此方法就是handle IMS +1404行的MSG_SWITCH_KEYBOARD_LAYOUT message。它主要做了两件事:
| 1 | /frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp | 
主要工作是trigger InputReader::requestRefreshConfiguration()
gityuan的一篇博客中清晰地介绍了InputReader, 对理解下面的代码会很有帮助。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72void InputReader::requestRefreshConfiguration(uint32_t changes) {
    AutoMutex _l(mLock);
    if (changes) {
        bool needWake = !mConfigurationChangesToRefresh;
        mConfigurationChangesToRefresh |= changes;
        if (needWake) {
            mEventHub->wake();
        }
    }
}
...
void InputReader::loopOnce() {
    int32_t oldGeneration;
    int32_t timeoutMillis;
    bool inputDevicesChanged = false;
    Vector<InputDeviceInfo> inputDevices;
    { // acquire lock
        AutoMutex _l(mLock);
        oldGeneration = mGeneration;
        timeoutMillis = -1;
        uint32_t changes = mConfigurationChangesToRefresh;
        if (changes) {
            mConfigurationChangesToRefresh = 0;
            timeoutMillis = 0;
            refreshConfigurationLocked(changes);
        } else if (mNextTimeout != LLONG_MAX) {
            nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
            timeoutMillis = toMillisecondTimeoutDelay(now, mNextTimeout);
        }
    } // release lock
...
}
...
void InputReader::refreshConfigurationLocked(uint32_t changes) {
    mPolicy->getReaderConfiguration(&mConfig);
    mEventHub->setExcludedDevices(mConfig.excludedDeviceNames);
    if (changes) {
...
        if (changes & InputReaderConfiguration::CHANGE_MUST_REOPEN) {
            mEventHub->requestReopenDevices();
        } else {
            for (size_t i = 0; i < mDevices.size(); i++) {
                InputDevice* device = mDevices.valueAt(i);
                device->configure(now, &mConfig, changes);
            }
        }
    }
}
...
void InputDevice::configure(nsecs_t when, const InputReaderConfiguration* config, uint32_t changes) {
...
    if (!isIgnored()) {
        if (!changes) { // first time only
            mContext->getEventHub()->getConfiguration(mId, &mConfiguration);
        }
        if (!changes || (changes & InputReaderConfiguration::CHANGE_KEYBOARD_LAYOUTS)) {
            if (!(mClasses & INPUT_DEVICE_CLASS_VIRTUAL)) {
                sp<KeyCharacterMap> keyboardLayout =
                        mContext->getPolicy()->getKeyboardLayoutOverlay(mIdentifier); (见2.4)
                if (mContext->getEventHub()->setKeyboardLayoutOverlay(mId, keyboardLayout)) {
                    bumpGeneration();(见2.5)
                }
            }
        }
...
}
这部分的代码主要工作是
如Gityuan博客中提到的,InputReader的成员变量mPolicy都是指NativeInputManager对象。这里getPolicy()得到的就是NativeInputManager对象。接下来我们看一下getKeyboardLayoutOverlay方法的具体工作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
public:
    NativeInputManager(jobject contextObj, jobject serviceObj, const sp<Looper>& looper);
...
    /* --- InputReaderPolicyInterface implementation --- */
...
    virtual sp<KeyCharacterMap> getKeyboardLayoutOverlay(const InputDeviceIdentifier& identifier);
...
int register_android_server_InputManager(JNIEnv* env) {
    int res = jniRegisterNativeMethods(env, "com/android/server/input/InputManagerService",
            gInputManagerMethods, NELEM(gInputManagerMethods));
    (void) res;  // Faked use when LOG_NDEBUG.
    LOG_FATAL_IF(res < 0, "Unable to register native methods.");
    // Callbacks
    jclass clazz;
    FIND_CLASS(clazz, "com/android/server/input/InputManagerService");
...
    GET_METHOD_ID(gServiceClassInfo.getKeyboardLayoutOverlay, clazz,
            "getKeyboardLayoutOverlay",
            "(Landroid/hardware/input/InputDeviceIdentifier;)[Ljava/lang/String;");
主要工作就是通过JNI调用JAVA层的InputManagerService.getKeyboardLayoutOverlay()方法。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27    // Native callback.
    private String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
        if (!mSystemReady) {
            return null;
        }
        String keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
        if (keyboardLayoutDescriptor == null) {
            return null;
        }
        final String[] result = new String[2];
        visitKeyboardLayout(keyboardLayoutDescriptor, new KeyboardLayoutVisitor() {
            @Override
            public void visitKeyboardLayout(Resources resources,
                    int keyboardLayoutResId, KeyboardLayout layout) {
                try {
                    result[0] = layout.getDescriptor();
                    result[1] = Streams.readFully(new InputStreamReader(
                            resources.openRawResource(keyboardLayoutResId)));
                } catch (IOException ex) {
                } catch (NotFoundException ex) {
                }
            }
        });
...
    }
可以看到它的主要工作是
从2.4节得到的keyboardlayout result作为参数,通过EventHub设置给相应的Device。1
2
3
4
5
6
7
8
9
10
11
12
13
14bool EventHub::setKeyboardLayoutOverlay(int32_t deviceId,
        const sp<KeyCharacterMap>& map) {
    AutoMutex _l(mLock);
    Device* device = getDeviceLocked(deviceId);
    if (device) {
        if (map != device->overlayKeyMap) {
            device->overlayKeyMap = map;
            device->combinedKeyMap = KeyCharacterMap::combine(
                    device->keyMap.keyCharacterMap, map);
            return true;
        }
    }
    return false;
}
最终实现为PK设置非英语的keyboard layout。
Native callback 感觉是最有意思的地方,以后可以用一下,JNI层还是需要多加研究的。对于Input系统,算是通过这次机会了解到这么多细节的东西,但是还有InputDispatch的部分还需要日后仔细学习。