做为一个桌面环境,输入法是必不可少的,输入法的切换也应该按照桌面的使用方式,control + shift 或者 control + space 进行切换,OpenFDE又是一个安卓桌面,所以需要对其输入法的切换进行改造,笔者暂时只实现了快捷键切换系统输入法,然而实际上,安卓的输入法与桌面使用方式还有一些不同之处,包括配置,键盘,提示词等等。
首先,从系统角度来认识一下安卓的输入法框架,他包含哪些内容,各模块之间又是什么关系,有了基本认识以后,再对比需求,尝试功能实现方式。
输入法管理服务的整体框架
输入法框架包含以下部分:
InputMethodManagerService
输入法系统服务(InputMethodManagerService),简称IMMS,由SystemServer启动,所以也是运行在system_server进程。MultiClientInputMethodManagerService是多会话输入法管理服务,主要应用在多屏设备上,支持每个会话使用不同的输入法功能。
源码位于 frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
系统输入法的主要逻辑全都在这个类里面实现,包含了输入法的所有管理功能:
com.android.server.inputmethod.InputMethodManagerService#setInputMethodLocked //设置输入法 com.android.server.inputmethod.InputMethodManagerService#switchToNextInputMethod //切换输入法 com.android.server.inputmethod.InputMethodManagerService#showInputMethodMenu //显示输入法菜单 com.android.server.inputmethod.InputMethodManagerService#onShellCommand //响应shell命令
InputMethodService
输入法服务(InputMethodService),简称IMS,三方输入法要继承实现这个类,当你要自己开发一个输入法的时候,就是通过继承这个service,注册到系统,提供其他应用使用,具体可参考官方文档。他是由IMMS启动,启动函数是startInputOrWindowGainedFocus,这个主要由InputMethodManager控制。
源码位于 frameworks/base/core/java/android/inputmethodservice/InputMethodService.java
输入法服务是输入法的具体实现,包含了每个输入法的所有功能:
android.inputmethodservice.InputMethodService#onCreateInputView //输入法键盘view android.inputmethodservice.InputMethodService#onCreateCandidatesView //提示词view android.inputmethodservice.InputMethodService#getCurrentInputConnection //处理文本的InputConnection android.inputmethodservice.InputMethodService#switchInputMethod(java.lang.String, android.view.inputmethod.InputMethodSubtype) //切换输入法,后文会讲解
InputMethodManager
输入法管理器(InputMethodManager),简称IMM,熟悉安卓架构的同学都理解,xxxManager是系统服务暴露给应用端的功能接口,使用系统服务基本功能在这个类里面就可以调用,但是又应该都理解,使用xxxManager限制非常多(也是因为各种hook技术),当你有一个需求的时候他大概率不能满足。APP一般会使用这个类来处理输入法,包含输入法唤起,软键盘,切换弹框等功能:
android.view.inputmethod.InputMethodManager#showInputMethodPicker //输入法切换弹框 android.view.inputmethod.InputMethodManager#showSoftInput(android.view.View, int) //显示软键盘,唤起输入法
对于没有键盘的手机来说,软键盘是必不可少的,showsoftinput就是唤起来输入法,当然安卓也提供了软键盘的控制,怎么显示,显不显示。
以下是显示/隐藏输入法的时序图:
以上只是概述了Android输入法的整体框架功能,具体调用逻辑,实现细节可以从源码中再做研究,或者可以参考这个链接。
输入法调试方法
输入法的安装会注册service,会由PackageManager管理,使用状态保存在系统数据库settings(现版本已保存在xml),输入法相关的数据保存在存储位置:/data/system/users/0/settings_secure.xml。
enabled_input_methods 已使能输入法 <setting id="1638" name="default_input_method" value="com.android.inputmethod.latin/.LatinIME" package="android" defaultValue="com.android.inputmethod.latin/.LatinIME" defaultSysSet="true" preserve_in_restore="true" /> default_input_method 默认输入法 <setting id="1374" name="enabled_input_methods" value="com.android.inputmethod.latin/.LatinIME:com.iflytek.inputmethod/.FlyIME" package="android" defaultValue="com.android.inputmethod.latin/.LatinIME:com.iflytek.inputmethod/.FlyIME" defaultSysSet="true" preserve_in_restore="true" />
enabled_input_methods是在设置里面管理屏幕键盘打开开关,打开之后,才能设置为default_input_method,而default_input_method才是真正使用的输入法。
可以通adb命令设置使能输入法和默认输入法;
查询使能的输入法
C:\Users\huyan> adb shell ime list -s com.android.inputmethod.latin/.LatinIME all.one.test/.AndroidInputMethodService com.iflytek.inputmethod/.FlyIME C:\Users\huyan> adb shell settings get secure enabled_input_methods com.android.inputmethod.latin/.LatinIME:all.one.test/.AndroidInputMethodService:com.iflytek.inputmethod/.FlyIME
设置默认输入法
adb shell settings put secure default_input_method com.iflytek.inputmethod/.FlyIME
查询默认输入法
C:\Users\huyan> adb shell settings get secure default_input_method com.iflytek.inputmethod/.FlyIME
这些命令的执行逻辑在上面说到的com.android.server.inputmethod.InputMethodManagerService#onShellCommand,如果在应用进程也是不能直接使用的。
快捷键切换输入法实现
输入法的切换其实有两个范畴,InputMethodInfo和InputMethodSubtype,InputMethodInfo对应的是每一个IMS,即一个输入法,InputMethodSubtype是输入法的子类型,比如不同语言,不同输入type,输入法本身做切换是切换子类型,本文实现的是切换整个输入法。
应用进程输入法切换的两种方式
一种是通过android.view.inputmethod.InputMethodManager#showInputMethodPicker,显示系统弹框给用户确认,这部分逻辑在com.android.server.inputmethod.InputMethodManagerService#showInputMethodMenu。输入法的确认逻辑在com.android.server.inputmethod.InputMethodManagerService#setInputMethodLocked。也就是说如果你是一个安卓应用,你想要切换输入法,必须弹窗用户确认来选择。
另一种是在IMM和IMS都提供了切换输入法的接口,先看IMM的switchToNextInputMethod函数如下,此函数已经废弃,InputMethodService#switchToNextInputMethod来替代,先不管,尝试一下,确实已经不能生效。
/** * Force switch to the next input method and subtype. If there is no IME enabled except * current IME and subtype, do nothing. * @param imeToken Supplies the identifying token given to an input method when it was started, * which allows it to perform this operation on itself. * @param onlyCurrentIme if true, the framework will find the next subtype which * belongs to the current IME * @return true if the current input method and subtype was successfully switched to the next * input method and subtype. * @deprecated Use {@link InputMethodService#switchToNextInputMethod(boolean)} instead. This * method was intended for IME developers who should be accessing APIs through the service. * APIs in this class are intended for app developers interacting with the IME. */ @Deprecated public boolean switchToNextInputMethod(IBinder imeToken, boolean onlyCurrentIme) { return InputMethodPrivilegedOperationsRegistry.get(imeToken) .switchToNextInputMethod(onlyCurrentIme);
此函数的实现是com.android.internal.inputmethod.InputMethodPrivilegedOperations#switchToNextInputMethod
public boolean switchToNextInputMethod(boolean onlyCurrentIme) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return false; } try { return ops.switchToNextInputMethod(onlyCurrentIme); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } |
IInputMethodPrivilegedOperations是Aidl生成的IMMS的远程接口,ops如果为null,那功能就不生效。如果不能提供有效的imeToken,就无法调用这个接口。
InputMethodPrivilegedOperationsRegistry.get(imeToken) .switchToNextInputMethod(onlyCurrentIme); |
再看IMS的switchInputMethod函数,这个是有效的,即提供给IME开发者使用这个API,因为输入法本身就能拿到imeToken。
/** * Force switch to a new input method, as identified by {@code id}. This * input method will be destroyed, and the requested one started on the * current input field. * * @param id Unique identifier of the new input method to start. * @param subtype The new subtype of the new input method to be switched to. */ public final void switchInputMethod(String id, InputMethodSubtype subtype) { mPrivOps.setInputMethodAndSubtype(id, subtype); } |
在应用进程如果要用API来切换输入法,就只能在输入法内部来调用,那样系统就凭空多出来一个输入法,而且切换完了之后,本身的IMS就退出了,没法继续进行切换。
当然,Setting应用做为系统设置应用也提供了修改输入法的接口,可以参考这个链接。
系统进程切换输入法
本文在IMMS来实现切换,并没有修改IMMS对外的接口,新增的逻辑也尽量不要修改现有逻辑。
首先系统快捷键在系统快捷拦截函数com.android.server.policy.PhoneWindowManager#interceptKeyBeforeDispatching,找到现在Android系统已有的处理切换的逻辑。但是他这个切换只是换了键盘布局并没有,切换输入法,这也是Android切换输入法操作和桌面操作的不同。
// Handle keyboard language switching. final boolean isCtrlOrMetaSpace = keyCode == KeyEvent.KEYCODE_SPACE && (metaState & (KeyEvent.META_CTRL_MASK | KeyEvent.META_META_MASK)) != 0; if (down && repeatCount == 0 && (keyCode == KeyEvent.KEYCODE_LANGUAGE_SWITCH || isCtrlOrMetaSpace)) { int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1; mWindowManagerFuncs.switchKeyboardLayout(event.getDeviceId(), direction); return -1; } 改为: // Handle keyboard language switching. final boolean isCtrlOrMetaSpace = keyCode == KeyEvent.KEYCODE_SPACE && (metaState & (KeyEvent.META_CTRL_MASK | KeyEvent.META_META_MASK)) != 0; if (down && repeatCount == 0 && (keyCode == KeyEvent.KEYCODE_LANGUAGE_SWITCH || isCtrlOrMetaSpace)) { int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1; Slog.w(TAG, "direction:"+direction + " deviceid:" + event.getDeviceId()); mWindowManagerFuncs.switchKeyboardLayout(event.getDeviceId(), direction); if (mInputMethodManagerInternal == null) { mInputMethodManagerInternal = LocalServices.getService(InputMethodManagerInternal.class); } mInputMethodManagerInternal.switchToNextInputMethod(false); return -1; } |
InputMethodManagerInternal 是IMMS的代理类,需要在IMMS里面做真正的实现。
主要修改了以下函数:
frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@BinderThread private boolean switchToNextInputMethod(boolean onlyCurrentIme) { synchronized (mMethodMap) { final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked( onlyCurrentIme, mMethodMap.get(mCurMethodId), mCurrentSubtype); if (nextSubtype == null) { return false; } //全局修改,不需要传入token setInputMethodWithSubtypeIdLocked(null, nextSubtype.mImi.getId(), nextSubtype.mSubtypeId); return true; } } |
services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
//分成了两个list,需要交叉遍历 //mSwitchingAwareRotationList //mSwitchingUnawareRotationList public ImeSubtypeListItem getNextInputMethod(boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { if (imi == null) { return null; } // if (imi.supportsSwitchingToNextInputMethod() && nextIndex <= mSwitchingAwareRotationList.mImeSubtypeList.size() - 1) { // return mSwitchingAwareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi, // subtype); // } else { // return mSwitchingUnawareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi, // subtype); // } if ( nextIndex <= mSwitchingAwareRotationList.mImeSubtypeList.size() - 1) { nextIndex++; return mSwitchingAwareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi, subtype); } else { if(nextIndex == mSwitchingAwareRotationList.mImeSubtypeList.size() + mSwitchingUnawareRotationList.mImeSubtypeList.size() - 1){ nextIndex = 0; } else { nextIndex++; } ImeSubtypeListItem itmes = mSwitchingUnawareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi, subtype); return itmes; } } |
supportsSwitchingToNextInputMethod是从每个输入法的属性取出来的,这里全局切换不能使用这个逻辑。
supportsSwitchingToNextInputMethod = sa.getBoolean( com.android.internal.R.styleable.InputMethod_supportsSwitchingToNextInputMethod, |
com.android.server.inputmethod.InputMethodSubtypeSwitchingController.DynamicRotationList#getNextInputMethodLocked
public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { int currentUsageRank = getUsageRank(imi, subtype); if (currentUsageRank < 0) { // if (DEBUG) { // Slog.d(TAG, "IME/subtype is not found: " + imi.getId() + ", " + subtype); // } // return null; //遍历一遍了,又从rank 下标0开始 currentUsageRank = 0; } final int N = mUsageHistoryOfSubtypeListItemIndex.length; //如果只有一个subtype就直接返回 if( N == 1){ return mImeSubtypeList.get(0); } for (int i = 1; i < N; i++) { final int subtypeListItemRank = (currentUsageRank + i) % N; final int subtypeListItemIndex = mUsageHistoryOfSubtypeListItemIndex[subtypeListItemRank]; final ImeSubtypeListItem subtypeListItem = mImeSubtypeList.get(subtypeListItemIndex); if (onlyCurrentIme && !imi.equals(subtypeListItem.mImi)) { continue; } return subtypeListItem; } return null; } |
com.android.server.inputmethod.InputMethodSubtypeSwitchingController#getNextInputMethodLocked
public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { if (imi == null) { return null; } //如果只有一个subtype就直接返回 if (mImeSubtypeList.size() == 0) { return null; } if (mImeSubtypeList.size() == 1) { return mImeSubtypeList.get(0); } final int currentIndex = getIndex(imi, subtype); if (currentIndex < 0) { return null; } final int N = mImeSubtypeList.size(); for (int offset = 1; offset < N; ++offset) { // Start searching the next IME/subtype from the next of the current index. final int candidateIndex = (currentIndex + offset) % N; final ImeSubtypeListItem candidate = mImeSubtypeList.get(candidateIndex); // Skip if searching inside the current IME only, but the candidate is not // the current IME. if (onlyCurrentIme && !imi.equals(candidate.mImi)) { continue; } return candidate; } return null; } |
此方法切换输入法的调用栈是:
com.android.server.policy.PhoneWindowManager#interceptKeyBeforeDispatching
com.android.server.inputmethod.InputMethodManagerInternal#switchToNextInputMethod
com.android.server.inputmethod.InputMethodManagerService.LocalServiceImpl#switchToNextInputMethod
com.android.server.inputmethod.InputMethodManagerService#switchToNextInputMethod
com.android.server.inputmethod.InputMethodManagerService#setInputMethodWithSubtypeIdLocked
com.android.server.inputmethod.InputMethodManagerService#setInputMethodLocked
void setInputMethodLocked(String id, int subtypeId)
只需要id,即可完成切换。至此,即完成了组合键control + space 实现切换系统输入法。
添加评论