前两天有同行在群里问了个挺实际的问题:做一个AOSP定制系统,客户要求设置里的“语言与输入法”列表,只能看到Gboard和AOSP键盘,其他输入法藏起来。这个需求听起来简单,真动手改原生设置时,很容易踩到“改了没效果”或者“系统设置崩了”的坑。我干脆把这次的完整思路和实操过程整理出来,从定位代码到过滤实现再到踩坑,一次说清楚。
这里先明确一下“原生设置输入法列表”指的是哪个界面:进入设置 -> 系统 -> 语言与输入法 -> 键盘和输入法,这里会列出当前用户已启用或者可启用的输入法(IME)。我们要做的事情,就是在这个列表的展示层面,通过包名(packageName)做白名单过滤,只让Gboard(包名一般是com.google.android.inputmethod.latin)和AOSP键盘(包名com.android.inputmethod.latin)出现在列表里。
1. 需求背景与前置认知
1.1 原生设置里的输入法列表是怎么来的
要过滤列表,先得搞清楚这个列表的数据来源。Android系统的输入法框架核心是InputMethodManagerService,它在system_server进程里运行,职责很简单:扫描系统里所有声明了输入法服务的应用,维护一份输入法清单,向设置应用和普通应用提供查询接口。
一个应用想成为输入法,必须在AndroidManifest.xml里声明一个Service,并且满足三个硬性条件:Service要带有android.view.InputMethod的 intent-filter,需要申请android.permission.BIND_INPUT_METHOD权限(这个权限是系统级保护权限),Service本身还要通过android:permission="android.permission.BIND_INPUT_METHOD"来限制绑定。三个条件缺一不可,否则即使装了应用,系统也不会把它当成输入法。
设置应用里的“键盘和输入法”列表,本质上是调用InputMethodManager.getInputMethodList()或getEnabledInputMethodList()来获取数据。前者返回所有已安装且声明合法的IME,后者只返回用户当前启用的IME。不同Android版本的设置界面展示策略有差异,但底层数据源就是这两个方法。
理解了这个流程,过滤的思路就清晰了:要么在数据源头(InputMethodManagerService)把非白名单输入法干掉,要么在设置应用展示列表时做一次包名过滤。两条路都能实现“设置列表里只保留Gboard和AOSP键盘”,但影响范围和代码位置完全不同,这个在后面章节专门讲。
1.2 为什么是“包名过滤”,而不是直接卸载或停用
拿到需求时,有的同学第一反应是:直接把其他输入法卸载或禁用不就行了?这个思路在纯个人使用场景没问题,但在真实定制项目里基本行不通,原因有三个。
第一,很多输入法是系统预置应用,厂商或客户要求必须保留,不能删。比如某行业设备里预装的语音输入法,或者系统里某个输入法还承担着专用键盘的功能,卸载会导致其他功能异常。第二,禁用应用(pm disable-user)虽然可以让输入法从设置列表消失,但它是全局的,会把整个应用的所有组件一起停掉,不够精确。第三,定制系统往往会预装三方的输入法,客户希望用户看不到但应用还在,这样后续通过OTA升级或者企业策略还能恢复启用。
包名过滤最大的优势就是“只影响展示,不影响存在”。输入法应用还在系统里,还是已安装状态,所有系统服务对该应用的引用都正常,只是设置列表把它藏起来。对于定制ROM或者企业设备,这是一个非常稳妥的处理方式。
2. 过滤位置的三种选择:选对地方很重要
2.1 方案A:在设置应用的UI层过滤
最直觉的做法,就是改packages/apps/Settings里的设置代码,在输入法列表展示前按包名白名单过滤。具体位置在“语言与输入法”页面对应的Fragment和对应的Controller里。
这个方案的优点很明显:改动范围限定在Settings应用内部,对系统的其他部分零影响。系统服务层仍然会向其他应用返回完整的输入法列表,比如微信、浏览器里切换输入法时,还是能看到所有输入法,只是设置界面里看不到。对于一部分需求场景,比如“只希望用户在设置里找不到其他输入法”,这个方案已经足够。
缺点也明显:如果你期望的过滤效果是“整个系统都只认这两个输入法”,UI层过滤就完全不够。输入法切换器(SystemUI里的键盘切换图标)、三方应用读取输入法列表等场景都会绕过设置,直接显示全部。
代码改动方面,需要找到设置里的输入法加载逻辑。新版AOSP里一般是在InputMethodPreferenceController或者对应的Loader中,拿到IME列表后先过滤再构造UI数据。这个方案对系统签名和权限要求最低,只需编译替换Settings应用即可。
2.2 方案B:在系统服务层过滤
更彻底的方案是直接改InputMethodManagerService,在它向外部返回输入法列表的方法里做包名过滤。因为所有获取输入法列表的请求最终都会经过这个服务,在这里过滤,等于一刀切:设置界面、输入法切换器、三方应用拿到的都只有白名单内的输入法。
但这里有个需要警惕的点:InputMethodManagerService是系统核心服务,改动它会影响系统里所有输入法相关功能。如果你只希望设置列表过滤,却改了系统服务层,用户在使用其他应用时可能发现输入法突然少了,而且排查起来非常隐蔽。所以这个方案通常只适合“整机只允许两种输入法”的定制场景,或者设备本来就是单业务用途。
代码上,主要修改集中在InputMethodManagerService的getInputMethodListLocked或类似的内部方法。需要在方法末尾对返回的列表做一次白名单过滤,同时注意保持列表的稳定性,别因为过滤导致索引错乱。
2.3 方案C:设备管理器白名单
还有一条企业级的路子,就是使用DevicePolicyManager.setPermittedInputMethods()接口。这是Android系统提供给设备所有者(Device Owner)或配置文件所有者(Profile Owner)用来限制可用输入法的标准API。
这个方案的好处是代码量极少,甚至不需要改系统源码,只需要一个设备管理应用在设备激活后调用API,传入允许的输入法包名列表。系统会自动保证这些包名之外的输入法被过滤,不需要自己维护过滤逻辑。
但这个方案的局限性也很明显:它要求设备必须有一个激活的设备所有者应用,而且API的过滤效果同样作用于整个系统,不只是设置列表。对于纯AOSP定制ROM来说,如果系统本来就要集成设备管理能力,这个方案很好;如果只是为了设置里藏几个输入法,为了这个需求专门引入设备管理器,有点杀鸡用牛刀。
2.4 三种方案怎么选
我在实际项目里一般按下面的逻辑判断:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 只在设置界面隐藏,系统其他位置不受影响 | 方案A(UI层过滤) | 改动风险最小,容易维护 |
| 整机只允许两个输入法,切换器、三方应用都看不到其他IME | 方案B(系统服务层过滤) | 全链路统一过滤,效果一致 |
| 设备已有Device Owner策略,需要动态管理允许列表 | 方案C(DevicePolicy API) | 标准API,支持运行时动态修改 |
如果你拿不准客户真实需求,先按方案A做,然后在验证时问清楚:“其他应用里切换输入法时,是不是也不能出现其他输入法?”如果客户说“是”,再升级到方案B,这样能避免一上来就改系统核心服务带来的一堆排查成本。
3. 实操:设置应用UI层按包名过滤
3.1 定位“语言与输入法”页面代码
在AOSP源码里找到设置应用的输入法入口,不同Android版本的位置略有区别。以Android 12及以后的版本为例,路径通常是:
packages/apps/Settings/src/com/android/settings/inputmethod/这里面主要关注三个文件:
LanguageAndInputSettings.java:语言与输入法设置页面,负责整体布局和子项跳转。InputMethodPreferenceController.java:所有输入法列表的Controller,负责加载和填充IME数据。InputMethodPreference.java:单个输入法的偏好设置项,负责展示图标、名称、状态和点击行为。
输入法列表的实际加载逻辑在Controller里。简单来说,Controller通过InputMethodManager.getInstance().getInputMethodList()拿到所有输入法列表,然后针对每个输入法创建一个InputMethodPreference添加到界面。
以Android 13/14版本为例,Controller里会有类似下面这样的代码逻辑(这里写的是简化后的真实路径):
public class InputMethodPreferenceController extends BasePreferenceController { @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); List<InputMethodInfo> imes = imm.getInputMethodList(); // 这里加载每个IME并添加preference } }如果你的AOSP版本比较旧(Android 10或11),对应的代码可能在LanguageAndInputSettings.java里的onResume方法中,直接通过循环创建InputMethodPreference添加。原理一样,只是代码组织方式不同。
3.2 白名单配置与核心过滤逻辑
定位到加载IME列表的地方后,过滤逻辑非常直接:拿到所有IME列表,逐个检查packageName,如果不在白名单里就跳过,不创建对应的preference。
我建议把白名单放在资源文件里,不要硬编码在代码中,这样后续要增减输入法,只需要改资源文件重新编译即可,维护成本低很多。
在res/values/arrays.xml里新增:
<!-- 允许在设置--语言与输入法列表中显示的输入法包名 --> <string-array name="config_allowed_ime_packages"> <item>com.google.android.inputmethod.latin</item> <item>com.android.inputmethod.latin</item> </string-array>然后在Controller的加载逻辑里增加一个过滤函数:
private List<InputMethodInfo> filterImeByPackage(List<InputMethodInfo> imeList) { String[] allowedPackages = mContext.getResources() .getStringArray(R.array.config_allowed_ime_packages); Set<String> allowedSet = new HashSet<>(Arrays.asList(allowedPackages)); List<InputMethodInfo> filteredList = new ArrayList<>(); for (InputMethodInfo ime : imeList) { if (allowedSet.contains(ime.getPackageName())) { filteredList.add(ime); } } return filteredList; }注意:InputMethodInfo还有getServiceName()方法,返回的是“包名 + Service类名”的完整组件名。如果需求是“同一个包下只要某个输入法服务”,那就要用getServiceName()来匹配,而不是getPackageName()。我们的需求是按包名过滤,所以用包名就够了。
在原来的加载代码里,把imm.getInputMethodList()的返回值换成filterImeByPackage(...)的结果,再继续创建preference,UI层过滤就完成了。
3.3 编译、替换与验证
改完代码,编译替换Settings应用。AOSP编译Settings的方法:
source build/envsetup.sh lunch <your-product>-userdebug make Settings -j8编译产物一般在out/target/product/<your-product>/system/priv-app/Settings/Settings.apk。在userdebug版本上,可以直接通过adb推送到系统目录替换:
adb root adb remount adb push out/target/product/<product>/system/priv-app/Settings/Settings.apk /system/priv-app/Settings/Settings.apk adb shell chmod 644 /system/priv-app/Settings/Settings.apk adb reboot重启后进入设置,语言与输入法页面应该只会看到Gboard和AOSP键盘两个输入法。
这里有个很重要的验证点:如果系统里还有其他输入法,但都被过滤掉了,要确认“当前使用的默认输入法”逻辑正常。老版本Android在输入法全部被过滤时,Settings进程可能直接崩溃,建议先确认Gboard或AOSP键盘至少有一个处于启用状态,再做替换。如果默认输入法被设置成了某个被过滤的输入法,设置页虽然不显示它,但系统仍然在用,容易造成界面和实际状态不一致。
验证时还要注意:设置应用的列表有时候会走缓存。替换apk后如果看到旧列表,先强制停止设置再重新打开:
adb shell am force-stop com.android.settings adb shell am start -a android.settings.INPUT_METHOD_SETTINGS4. 进阶:在系统服务层把过滤做彻底
4.1 修改InputMethodManagerService的关键代码
如果客户要求“整个系统只能看到Gboard和AOSP键盘”,那就要深入到系统服务层。InputMethodManagerService的源码位于:
frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodManagerService.javaAndroid 12之后的版本里,外部查询输入法列表会走getInputMethodList()和getEnabledInputMethodList(),这两个方法内部都会进入一个带锁的方法。我需要关注的是getInputMethodListLocked()这类收集所有IME的方法。
可以这样改:在返回列表前做一次过滤,只保留白名单包名对应的InputMethodInfo。
private List<InputMethodInfo> filterAllowedInputMethods( List<InputMethodInfo> allImeList) { List<InputMethodInfo> result = new ArrayList<>(); for (InputMethodInfo ime : allImeList) { String pkg = ime.getPackageName(); if (pkg.equals("com.google.android.inputmethod.latin") || pkg.equals("com.android.inputmethod.latin")) { result.add(ime); } } return result; }然后在getInputMethodListLocked的 return 处调用这个函数。改完需要重新编译整个系统镜像,因为system_server属于核心进程,没法单独替换一个apk来解决,必须整包刷机。
这里要特别提醒:如果是在Android 11或更早的版本上操作,方法名和内部结构差异很大。有些版本里,列表过滤还需要同步处理mMethodMap这个内部缓存,否则会出现“重启后恢复”或者“列表时好时坏”的问题。动手前先花二十分钟把你当前源码里所有调用getInputMethodList的链路捋清楚,别只改了一个入口。
4.2 服务层过滤的副作用控制
服务层过滤最大的隐患不是找不到代码,而是改完之后的连带影响。
第一,Settings和SystemUI的输入法切换器读取的是同一份数据源。服务层过滤后,切换器里其他输入法会消失,这是预期的效果。但有些应用在调用输入法列表时,会默认取列表的第一个输入法作为备选,过滤后这个“第一个”很可能发生变化,应用行为也随之变化,需要回归测试。
第二,还要考虑“键盘和应用键盘弹出的默认逻辑”。如果当前用户的默认输入法被过滤掉,系统在启动输入时可能找不到可用的IME,导致键盘弹不出来。所以在服务层过滤时,最好同步修改系统配置里的默认输入法,把它固定成白名单内的包名。具体在frameworks/base/core/res/res/xml/config.xml里,找到config_defaultIme相关的配置项,改成Gboard或AOSP键盘的默认ImeId。
第三,InputMethodManagerService里还有一组onUnlockUser、onSwitchUser之类的回调,会在用户切换时重新加载输入法列表。如果只改了查询入口,没处理这些回调里对列表的引用,可能在切换用户后出现空列表或者崩溃。建议过滤逻辑封装成一个独立方法,在所有读取IME列表的位置统一调用。
服务层过滤是“收益最大、代价也最大”的做法。做之前一定要确认需求真的需要“全系统级过滤”,同时安排足够的回归测试时间。我见过不止一次因为服务层过滤导致个别应用崩溃,最后排查半天才发现是输入法列表数量变化引起的。
5. 我踩过的坑与排查技巧
5.1 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 设置列表过滤了,但输入法切换器仍然显示其他IME | 只改了UI层,没改系统服务层 | 按方案B修改InputMethodManagerService |
| 过滤后设置页空白或进程崩溃 | 全部IME都被过滤,或默认IME被过滤 | 至少保留一个白名单IME,并把默认IME设成白名单内 |
| 列表里看到的是旧的IME数据 | Settings没刷新或走了缓存 | 强制停止设置应用,或清空设置应用数据后重进 |
| 过滤规则不生效,重启恢复 | 改错方法入口,内部缓存未过滤 | 检查mMethodMap相关缓存,所有读取入口统一过滤 |
| 三方应用崩溃 | 服务层过滤后IME列表数量变化 | 检查应用对IME列表索引的依赖,评估过滤影响 |
| Gboard包名不匹配 | 设备上的Gboard被厂商改了包名 | 通过dumpsys package或pm list packages确认实际包名 |
5.2 三个实操建议
第一个建议:过滤条件尽量做成配置化。不管是UI层还是服务层,把包名白名单放在资源文件里比写死在代码里强很多。定制项目中经常出现“客户今天说只要两个输入法,下个月又要求加一个”,写死在代码里每次都要重新梳理逻辑,配置化改一行资源就能搞定。
第二个建议:过滤后一定要做多用户验证。输入法列表是用户相关的,如果有访客用户或者多用户空间,新用户首次启动时默认IME的初始化逻辑可能被过滤行为影响。我在一次项目里过滤后,第二个用户首次进系统键盘弹不出来,排查发现是默认IME状态在User 0里被过滤,新用户继承状态时找不到对应服务。这个问题如果不去实际创建新用户,根本发现不了。
第三个建议:改InputMethodManagerService之前,先看你的版本里有没有其他模块引用了输入法列表的索引或顺序。因为在某些版本里,InputMethodManager公开API返回的列表顺序是固定的,过滤打乱顺序后,一些应用可能记录了下标,下次用下标取输入法时拿到错误对象,表现就是“键盘没弹出来”或“弹出的是另一个输入法”。
这次做下来,我个人最大的体会是:UI层过滤升级到系统服务层过滤之间,隔着一道“需求确认”的鸿沟。表面上看只是代码改哪里的区别,实际上涉及整个系统对输入法生态的感知。建议拿到需求后先画一条线:过滤边界到底在哪。边界内的改动,代码再复杂都相对安全;边界一旦模糊,改动就会波及很多无法预料的场景,后续排查的成本会翻着倍往上涨。