基于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的部分还需要日后仔细学习。