Android方便键切换输入法

做为一个桌面环境,输入法是必不可少的,输入法的切换也应该依照桌面的运用办法,control + shift 或许 control + space 进行切换,关于一个安卓桌面,所以需求对其输入法的切换进行改造,笔者暂时只完成了方便键切换体系输入法,但是实际上,安卓的输入法与桌面运用办法还有一些不同之处,包括配置,键盘,提示词等等。

首要,从体系角度来认识一下安卓的输入法结构,他包括哪些内容,各模块之间又是什么关系,有了基本认识今后,再对比需求,测验功用完成办法。

输入法办理服务的全体结构

Android方便键切换输入法
输入法结构包括以下部分:

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方便键切换输入法

以上仅仅概述了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才是真实运用的输入法。

Android方便键切换输入法

能够通adb命令设置使能输入法和默许输入法

查询使能的输入法

C:Usershuyan> adb shell ime list -s
com.android.inputmethod.latin/.LatinIME
all.one.test/.AndroidInputMethodService
com.iflytek.inputmethod/.FlyIME
C:Usershuyan> 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:Usershuyan> 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。也便是说假如你是一个安卓运用,你想要切换输入法,必须弹窗用户承认来选择。

Android方便键切换输入法

另一种是在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() <= 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;
        }

此办法切换输入法的调用栈是:

  1. com.android.server.policy.PhoneWindowManager#interceptKeyBeforeDispatching
  2. com.android.server.inputmethod.InputMethodManagerInternal#switchToNextInputMethod
  3. com.android.server.inputmethod.InputMethodManagerService.LocalServiceImpl#switchToNextInputMethod
  4. com.android.server.inputmethod.InputMethodManagerService#switchToNextInputMethod
  5. com.android.server.inputmethod.InputMethodManagerService#setInputMethodWithSubtypeIdLocked
  6. com.android.server.inputmethod.InputMethodManagerService#setInputMethodLocked
void setInputMethodLocked(String id, int subtypeId)

只需求id,即可完成切换。至此,即完成了组合键control + space 完成切换体系输入法。