版本比较

密钥

  • 该行被添加。
  • 该行被删除。
  • 格式已经改变。
目录
minLevel1
maxLevel4
outlinefalse
stylenone
typelist
printablefalse

...

这些命令的执行逻辑在上面说到的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来替代,先不管,尝试一下,确实已经不能生效。

代码块
languagejava
/**
 * 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

代码块
languagejava
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,就无法调用这个接口。

代码块
languagejava
InputMethodPrivilegedOperationsRegistry.get(imeToken)
       .switchToNextInputMethod(onlyCurrentIme);

再看IMS的switchInputMethod函数,这个是有效的,即提供给IME开发者使用这个API,因为输入法本身就能拿到imeToken。

代码块
languagejava
   /**
    * 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切换输入法操作和桌面操作的不同。

代码块
languagejava
         // 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

代码块
languagejava
@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

代码块
languagejava
//分成了两个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是从每个输入法的属性取出来的,这里全局切换不能使用这个逻辑。

代码块
languagejava
           supportsSwitchingToNextInputMethod = sa.getBoolean(
                   com.android.internal.R.styleable.InputMethod_supportsSwitchingToNextInputMethod,

com.android.server.inputmethod.InputMethodSubtypeSwitchingController.DynamicRotationList#getNextInputMethodLocked

代码块
languagejava
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

代码块
languagejava
       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;
       }

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

  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 实现切换系统输入法。