ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android系统定制:默认输入法设置与运行时权限授予实战

Android系统定制:默认输入法设置与运行时权限授予实战

说一个我踩过挺多坑的场景:在做Android定制系统或者企业设备批量部署时,经常需要把某款第三方输入法直接设成系统默认输入法,而且要保证它需要的运行时权限在一开机就被授予,否则同事拿到的设备上输入法不是弹不出来,就是语音输入能用但无法访问麦克风,体验非常割裂。

这篇文章就拿两类最典型的输入法来说:一类是国内主流第三方中文输入法,我下面叫它A输入法;另一类是海外原生风格的拼音输入法,我叫它B输入法。出于平台规范就不直接点名了,实际你遇到的大部分输入法逻辑都类似。文章会把配置默认输入法、管理运行时权限的原理和实操一次讲透,顺便把这个过程中我踩过的那些真坑一并列出来。

1. 为什么要在系统层设定默认输入法并处理运行时授权

1.1 这个需求通常从哪来

大部分人给手机装输入法,都是打开应用之后顺着引导一步步开启的。但如果你做的是行业定制设备、教育平板、车机系统,或者要给几十上百台设备做自动化测试,场景就完全不同了。你不希望用户去设置里自己翻找输入法,更不希望输入法第一次弹出来时因为没有存储权限而崩溃。这时候就需要在系统层预先完成两件事:把输入法设为默认,把敏感权限授予到位。

我最近接的一个项目就是类似情况:某公司在定制一批Android设备,预装了一款第三方中文输入法,要求开机后输入法默认生效,并且语音输入、联系人联想、剪贴板读取这些能力必须可用。A输入法在普通模式下会申请麦克风、存储、通讯录等多项权限,Android 6.0之后这些都属于运行时权限,系统需要用户逐个弹窗确认。如果设备上没有交互式配置环境,这个弹窗流程就会卡住功能。

这个需求落到技术实现上,就是两条线并行:一条是设置安全键值把输入法切换为默认,另一条是绕过运行时权限弹窗直接授予权限。前者属于系统设置管理,后者涉及Android权限模型的应用,单独拎出哪一条都不难,但放在一起就容易混淆。

1.2 输入法APP的权限模型和普通APP不一样

我见过不少开发者把输入法当成普通应用处理,其实这里有个关键差异。普通应用只要在AndroidManifest里声明权限,用户安装时在弹窗里点几下授权就结束了。输入法不一样,它首先是一个服务(Service),需要在系统的输入法管理框架里注册,然后还要在系统设置里被用户“启用”,最后才能被选成默认输入法。

这个注册和普通Activity的应用完全不同。输入法服务需要在manifest里声明android.permission.BIND_INPUT_METHOD,这个权限是系统专属的,第三方应用无法授予。这也是为什么输入法APK必须经过系统认证才能被识别为输入法,你随便拿一个普通应用伪装成IME是不行的。

同时,输入法还需要一个InputMethodService的子类,并且在res/xml/method.xml里配置了<input-method>标签,包含settingsActivity等元数据。只有满足这些条件,系统设置里的“语言与输入法”面板才会列出这个输入法。如果预置的是A输入法,通常会包含一个完整的中文输入服务,而B输入法则可能额外带拼音、笔画、语音等多种子键盘,注册的组件可能有多个。

1.3 几条必须知道的Android机制

要实现“默认输入法+运行时授权”,需要先记住几个系统层面的机制。

第一,默认输入法不是存到普通存储区的。系统通过Settings.Secure表保存相关配置,具体键名是default_input_method和enabled_input_methods。这两把钥匙只有系统App或者有WRITE_SECURE_SETTINGS权限的App才能修改。在普通App里直接调用Settings.Secure.putString是会报SecurityException的,所以常规做法是使用ADB命令修改。

第二,运行时权限的存储也不在Settings.Secure里。运行时权限是Android 6.0引入的动态权限模型,权限授予状态存储在系统服务里的AppOps或者PackageManager的权限状态表中。第三方普通App能查询,但没有授权能力。要绕过弹窗,要么用ADB的pm grant命令,要么让App以特权应用身份预置到系统分区并配置默认授权。

第三,输入法激活需要IME服务有焦点回调。就算你硬把默认输入法设置好了,如果该输入法的IME Service在运行时被系统杀死或者出现异常,系统可能自动回退到原有输入法,页面也会提示“当前输入法无法使用”。这点在后期排查权限问题时尤其重要。

记住以上三点,后面的所有操作都是围绕它们展开的。

2. 核心概念拆解:默认输入法、已启用输入法、运行时权限

2.1 输入法(IME)在Android中的注册与启用

输入法的完整生命周期是这样的:APK被安装后,系统通过InputMethodManagerService扫描包含BIND_INPUT_METHOD权限且带有InputMethodService子类的应用,把它们登记为可用的输入法。但这一步只是“系统认识你了”,用户还需要进入“设置 -> 语言与输入法”手动把该输入法后面的开关打开,也就是所谓的“启用”。

启用操作的本质,是在Settings.Secure的enabled_input_methods字段里追加一项。这个字段的值格式一般是用冒号分隔的IME完整组件名,比如:

com.example.ime/.ImeService:com.example.other/.PinyinService

系统会解析这个字段,判断哪些输入法可以被切换。如果你没有启用输入法就直接设置默认值,系统会忽略这个设置,默认输入法仍然保持原来的空事件或无输入法状态。

这也是为什么标题里“搜狗输入法或者谷歌拼音输入法”这类品牌输入法经常被拿来测试,因为它们的服务组件名是固定的、可查的,你可以在dumpsys输出里找到准确的service类名。但实际项目中,不同输入法的组件名不一样,改造时一定要先确认目标APK的IME Service完整路径。

2.2 默认输入法的存储位置与作用域

默认输入法字段也属于Settings.Secure,键名是default_input_method,它只存放一个值,就是你要默认启动的IME组件名。这个字段不是用户级的,而是系统级的,所有用户共享。在多用户或工作空间场景下,你需要考虑每个用户的空间是否都有输入法应用,否则切换用户后会看到默认输入法不可用的状态。

值得注意的是,即便不是系统应用,只要你拥有WRITE_SECURE_SETTINGS权限(例如通过ADB临时授予),也可以写入这个配置。ADB就是通过这种方式来修改设置的:

adb shell settings put secure default_input_method com.example.ime/.ImeService

实际执行时,settings命令会先检测当前Shell进程是否有权限访问Settings.Secure。使用ADB运行该命令时,通常具有特权,因此可以写入。但如果你是在App代码里用settings put或者直接调用数值,就需要额外配置。

2.3 运行时授权到底授的是什么权

很多人在这一步把“默认输入法设置”和“运行时权限授权”混为一谈,其实它们是完全两个不同的系统组件。

运行时授权指的是授予应用在运行时访问受保护资源的权限,比如录音、读取通讯录、读取存储、获取地理位置等。输入法可能申请的权限一般有:

  • RECORD_AUDIO:语音输入/语音听写必须。
  • READ_CONTACTS:联系人姓名联想。
  • READ_EXTERNAL_STORAGE/READ_MEDIA_*:读取本地方言词库或用户词库。
  • READ_CLIPBOARD/POST_NOTIFICATIONS:剪贴板识别或通知栏快捷启动。
  • MANAGE_EXTERNAL_STORAGE:少数输入法要求管理全部文件,这类比较激进,一般不建议默认授予。

授权状态由系统运行时权限服务管理。当IME尝试调用requestPermissions()时,系统会弹出权限对话框。如果IME没有在前台或设备没有交互,系统甚至不会弹窗,功能会直接被拒绝。这种场景就是必须用命令或特权配置预先授权的根本原因。

3. 实操:通过ADB完成配置和授权(以A输入法为例)

3.1 先完成基础配置:启用并设为默认

假设我们要把A输入法安装到设备上,并设成默认。安装完成后,按顺序执行两步操作。

第一步,查询输入法实际包名和Service类名。最稳妥的方式是用命令查看当前已识别的输入法列表:

adb shell ime list -s

输出类似:

com.google.android.inputmethod.pinyin/.PinyinIME com.example.aime/.AIME

第二行就是A输入法的服务项。如果列表里没有A输入法,说明APK本身或者预置方式有问题,后面再排查。

第二步,先写入已启用输入法列表,再写入默认输入法。这里有个顺序问题:顺序写反了容易导致设置不生效。命令如下:

adb shell settings put secure enabled_input_methods com.example.aime/.AIME adb shell settings put secure default_input_method com.example.aime/.AIME

执行完成后,最好再用命令确认一下写入结果:

adb shell settings get secure default_input_method

如果返回你刚写入的值,说明已经生效。不过仅这样还不够,部分系统在写入default_input_method后还需要触发一次输入法刷新。比较粗暴但有效的方法是重启一下系统UI,或者在测试时锁屏再点亮,你也可以发送广播让系统重新加载:

adb shell am broadcast -a android.intent.action.ACTION_INPUT_METHOD_SETTINGS

实测中这个广播不太可控,最稳定的是重启设备后验证。

3.2 运行时权限的授予方法

基础配置完成后,进入授权环节。假设A输入法需要麦克风和存储权限,先查看当前权限状态:

adb shell dumpsys package com.example.aime | grep -E "runtime permissions|granted=true"

这是一个模糊查询,实际输出会显示所有运行时权限的授予状态。你会发现RECORD_AUDIO可能是granted=false,这就是弹窗未走完导致的。

手动授权的命令是:

adb shell pm grant com.example.aime android.permission.RECORD_AUDIO adb shell pm grant com.example.aime android.permission.READ_EXTERNAL_STORAGE

其中pm grant只允许授予应用声明过的运行时权限。如果A输入法没有声明某个权限,执行命令会直接报错。另外,部分权限比较特殊,比如MANAGE_EXTERNAL_STORAGE这类需要用到appops命令单独处理,pm grant不支持。

授权之后再次用dumpsys确认,如果状态变成granted=true,就说明授权成功。如果你希望拒绝某项权限,可以用pm revoke进行撤销。

3.3 在代码中完成同样的操作(系统应用/特权应用方案)

使用ADB虽然方便,但只适合调试和小批量设备。真正到了量产阶段,你需要在系统镜像里预置一个配置脚本,或者在系统应用启动时用代码设置默认输入法和权限。

如果你有一个带系统签名的应用,可以在应用启动时利用Settings.Secure的putString接口写入默认输入法。同时,为了授予输入法运行时权限,可以把这个应用作为特权应用预置在/system/priv-app目录下,并使用privapp-permissions白名单声明WRITE_SECURE_SETTINGS或GRANT_RUNTIME_PERMISSIONS权限。

在Android 8.0以后,特权应用要声明白名单,否则系统会拒绝赋予额外权限。配置十分关键。你需要在/system/etc/permissions/下放置一个XML文件,格式类似:

<privapp-permissions package="com.example.admin"> <permission name="android.permission.WRITE_SECURE_SETTINGS"/> <permission name="android.permission.GRANT_RUNTIME_PERMISSIONS"/> </privapp-permissions>

然后在你系统应用里,通过PackageManager.grantRuntimePermission()方法给A输入法授权:

PackageManager pm = getPackageManager(); pm.grantRuntimePermission( "com.example.aime", "android.permission.RECORD_AUDIO", new UserHandle(0));

再配合:

Settings.Secure.putString( getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD, "com.example.aime/.AIME");

这样量产机开机后就可以直接使用A输入法,同时语音输入等依赖麦克风的功能也开箱可用。

这种方案不需要用户手动去设置里操作,非常适合车机、收银机、教育设备这类无人值守场景。

4. 针对象“某国内输入法/某海外拼音输入法”实际项目时的权益坑

4.1 各品牌输入法的服务和权限差异

我分别拿A输入法和B输入法做过同样一套配置,结果踩的坑完全不一样。

A输入法属于功能聚合型,它除了IME服务之外,可能还有独立的语音服务、词库更新服务、通知栏快捷开关等。它对外暴露的ImeService可能还会依赖其他组件的动态加载。直接设置默认输入法虽然能启动,但如果系统给它的动态连接不上,输入法会闪退或者只显示默认英文键盘。这种输入法在预置时,最好不要精简它的任何组件,否则就等于把核心输入功能拆掉了。

而且A输入法对权限的依赖非常大。它内部经常有“默认选择”的逻辑:如果某敏感权限没有授予,它会把相关入口置灰,而不是走一个平滑降级。比如没有录音权限,语音输入按钮是灰的,但你之前可能误以为是没有授权到所以导致输入法整体不可用,实际上输入法主体是正常的。

B输入法则比较干净,IME服务就是纯粹的输入法,语音输入是独立的模块,不常驻后台。它的权限请求相对更克制,主要是存储和麦克风。但B输入法的Service Path在不同版本上可能变化,早期版本叫PinyinIME,后来可能有其他扩展名,如果你对着网上教程里老旧的组件名去设置,会直接无效。

所以无论做哪类品牌,第一步永远是ime list -s,以实际设备输出为准。

4.2 授予权限后仍然无法正常使用怎么办

我遇到过一种情况:权限全给了,默认输入法也设置了,但A输入法在打开任意输入框时仍然提示“应用未获得全部权限”,同时系统设置里也会出现一个红色警告条,内容是“语音输入不可用”。

通过日志排查发现,A输入法在初始化时会通过PackageManager.checkSelfPermission检查权限,如果权限状态正常,它还要检查“权限是否是由用户主动授权”的状态。这是个从AppOps层面做的校验,某些输入法会记录权限来源。如果权限是通过pm grant授予的,它们内部可能判定为“非用户确认”,从而拒绝开启功能。

这种问题没有特别优雅的解法。最有效的办法是在预置时采用授予默认权限的方式,让应用在安装时就被标记为已授权。具体做法是在系统镜像中放置/etc/permissions/default-permissions.xml,示例:

<default-permissions> <grant-default-permissions> <package name="com.example.aime"> <permission name="android.permission.RECORD_AUDIO"/> <permission name="android.permission.READ_EXTERNAL_STORAGE"/> </package> </grant-default-permissions> </default-permissions>

这个文件需要系统一级的应用启动时加载,通常由DefaultPermissionGrantPolicy解析。它在首次应用安装后生效,会把配置的权限自动授予应用,效果等同于用户点过允许。

如果无法修改系统镜像,另一个偏方是先用ADB安装并授予权限,然后设置android:permission相关的系统属性和settings缓存,再重启设备。但实测因输入法版本而异,B输入法重来几次能扛过去,A输入法经常不行。

4.3 测试与验证

配置完以后,推荐按以下步骤做一轮回归测试:

  1. 设置默认输入法后,重启设备,进入短信或便签输入框,调起键盘。
  2. 验证中英文切换功能正常,确认默认键盘身份是A输入法或B输入法。
  3. 测试语音输入,看麦克风权限是否真正生效,录音是否正常。
  4. 测试联系人联想,确认通讯录权限有没有拦截。
  5. 打开设置里的“语言与输入法”,确认该输入法开关处于打开状态,并且没有被标记为“错误”。

如果在聊天类App中键盘无法调起,优先看系统自定义键盘相关的兼容性模式,有些国产ROM会做输入法沙盒,需要额外放行。

5. 常见问题与排查技巧实录

5.1 设置默认输入法后不生效

这是最高频的问题。原因通常是只设置了default_input_method,没有设置enabled_input_methods。如果你先写默认后写启用,某些系统会清掉默认设置,导致最后状态看起来还是没写进去。正确顺序务必是先启用、后默认。

还有一个隐蔽点:如果你同时启用了多个输入法,enabled_input_methods的值要用冒号分隔,且不能带多余空格。我曾见过配置脚本里多了一个空格,结果设置面板能识别,但系统切换时找不到组件名。

排查命令:

adb shell settings get secure enabled_input_methods adb shell settings get secure default_input_method

报价后如果发现默认值为空或者还是旧值,需要检查Shell是否有写权限。Android 11及以上部分设备对settings命令增加了限制,尤其是用户调试版,需要先执行:

adb shell cmd settings put secure default_input_method ...

而不是老式的settings put,两者行为在新版本上可能不同。

5.2 输入法权限被系统回收

部分定制系统有“权限回收”机制,比如长时间未使用某权限,会在后台自动撤销。这样即便你之前用pm grant成功,过几天还是会丢失。

这种问题没有完全通用解法,只能通过两条路解决:

  • 在自定义ROM里,把输入法包名加入权限白名单,禁止系统自动回收运行时权限。
  • 如果无法改ROM,做一个后台系统应用定期检查权限状态,发现被撤销就重新调用grantRuntimePermission。缺陷是这种方案需要系统应用或者至少要有GRANT_RUNTIME_PERMISSIONS权限,普通应用无法实现。

在A输入法场景下,它可能还会触发自己的权限守护,在权限被撤销后会把功能入口隐藏掉,用户会以为输入法坏了。所以排查时优先检查dumpsys package的权限状态,不要只看输入法界面。

5.3 第三方工具无法保存设置

很多人喜欢用设置编辑器类App去改默认输入法,这在老版本Android还有可能成功,到了Android 10以后基本没戏。原因很简单,普通App没有WRITE_SECURE_SETTINGS权限,而安全设置又不能被授予给非系统应用,即使你用ADB授予该权限,应用重启后也可能被系统收回。

所以务实的做法只有两个:用ADB做一次性调试,或者预置系统应用做量产管理。不用在第三方工具上浪费时间。

6. 最后再分享几个实用技巧

如果你是在项目初期验证输入法,建议先手输命令跑通流程,再封装成shell脚本。脚本里可以把包名、Service名、权限列表定义成变量,后续换输入法品牌时只改参数,不用重写逻辑。

我个人实测下来,最稳妥的组合是:系统里放一个小的特权管理应用,启动时读取配置列表,批量执行“启用输入法 + 默认输入法 + grantRuntimePermission”三连操作。这比用脚本好,因为脚本执行时机不好控制,有时系统服务还没启动完,命令就落盘失败。而应用的onCreate可以监听BOOT_COMPLETED广播,在系统广播之后运行,时间点更保险。

另外要注意,不同输入法版本对权限的要求会变。A输入法升级后可能会新增POST_NOTIFICATIONS权限,或者把存储权限切分成READ_MEDIA_IMAGES。建议每次升级APK后,都重新拉一遍dumpsys package,对比权限清单,别默认旧权限配置够用。

最后再提醒一句:给输入法授权要克制,只授予它实际要用的权限。有些输入法请求MANAGE_EXTERNAL_STORAGE或READ_SMS,这类敏感权限能不授就别授。真碰到了,就直接换一个权限需求简单的输入法,不然你设备的隐私审计这关就过不去。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进