做 Android14 系统定制的人,应该没少遇到过一类需求:把默认锁屏解锁方式改成“无”。我最近在跑 AOSP 14 项目时,客户明确提出开机后任何安全锁屏都不能出现,用户也不允许在设置里自己设置图案或密码,默认解锁方式必须是无锁屏。这个需求听起来简单,实际动起手来牵扯到 SettingsProvider、LockSettingsService、SystemUI 三块,稍不注意就会掉进“改了没生效”“编译不过”的坑里。这篇博文就是我个人基于 Android14 AOSP 源码的一次实操记录和避坑总结,主要面向做 ROM 定制、系统集成或者想深入理解锁屏机制的开发者。
1. 需求拆解:你想要的“无”到底指什么
1.1 三种不同形态的“无锁屏”
需求方把“默认锁屏解锁方式为无”这个需求提出来之后,先别急着改代码,一定要把“无”的定义对齐。我遇到过三种典型理解:
第一种,完全无锁屏。屏幕点亮之后直接进桌面,连滑动解锁这层都没有,用户在设置里也看不到“屏幕锁定”相关选项,或者看到了也改不了。自助终端、演示机、教学平板大多属于这种。
第二种,默认无安全锁。即初始状态是滑动解锁,不强制设置 PIN、图案或密码,但用户自己能进设置去开启安全锁屏。一般定制 ROM 的公共版固件通常是这种,既保证出厂的简洁体验,又保留用户后续设置的权利。
第三种,仅设置项默认显示为“无”。锁屏界面其实还有,只是系统设置里默认选中的是“无”。这种需求比较少见,但如果客户只是给了一张截图让你照着改,就容易误入这条路。
我在项目里遇到最多的是第一种,也就是“外观上完全不见锁屏,桌面即开即用”。这其实不只是改一个解锁方式的问题,而是要把锁屏这一整套前台体系处理掉。搞清楚需求定义再动手,后面能少走很多弯路。
1.2 Android14 里锁屏到底是怎么串起来的
Android 每次亮屏,SystemUI 的 KeyguardViewMediator 会判断当前是否显示 Keyguard,以及应该显示哪种风格的解锁界面。这个判断链里至少包括四部分:当前锁屏凭据类型(NONE、PATTERN、PIN、PASSWORD)、DevicePolicyManager 的锁屏策略、UserManager 里各用户的状态、以及 Settings 里一些历史遗留开关。
如果凭据类型是 NONE 且没有其他限制,Keyguard 并不是“不显示”,而是显示一个 Swipe(滑动解锁)卡片。所以原生 Android 的默认形态其实是“滑动解锁”,不是“无”。要想默认是无锁屏,必须让 Keyguard 在亮屏后直接 dismiss,或者压根不显示。
我习惯用一个类比来描述这个机制:Keyguard 是整个办公楼的闸机,安全锁是闸机加保安。你把保安撤了,闸机还在那里,进门还得推一下;把闸机也拆掉,才是真正无锁屏。所以“默认锁屏解锁方式为无”= 撤保安 + 撤闸机,而不是只把密码清空。这个认知不到位,改出来的效果一定是锁屏还在。
2. 核心改法与文件定位:动手前先看这几个地方
2.1 SettingsProvider 默认值仓库
系统设置的默认值大多集中在frameworks/base/packages/SettingsProvider/res/values/defaults.xml。SettingsProvider 在首次开机时会通过 DatabaseHelper 把这个 XML 导入到 settings 数据库中,分成 system、global、secure 三类。
与锁屏相关的常见项有:
lock_screen_lock_after_timeout:自动锁屏超时时间,单位毫秒。lock_screen_show_notifications:锁屏是否显示通知详情。lockscreen.disabled:早期版本中“禁用锁屏”的开关,对应Settings.Secure.LOCKSCREEN_DISABLED。
要注意,Android14 里这些值只是“数据库默认值”,不直接决定 Keyguard 是否显示。网上很多帖子让你只把lockscreen.disabled改成 1,实测下来亮屏依然会有滑动手势层,而且在部分版本上这个字段已经被边缘化,真正起作用的是 locksettings 的凭据状态和 SystemUI 的 KeyguardController。
改默认值的正确姿势是:在设备的 overlay 目录中覆盖这个 XML,不要直接去改 frameworks 源码,否则每次同步 AOSP 代码都要重新打补丁。overlay 目录一般长这样:
device/<vendor>/<board>/overlay/frameworks/base/packages/SettingsProvider/res/values/defaults.xml
然后在产品配置文件里确认 overlay 被打包进去。这样改动和主源码隔离,后续升级也好维护。
2.2 LockSettingsService 与凭据存储
真正决定“有没有锁”的模块是 LockSettingsService,它负责管理锁屏凭据的保存和校验,数据落在/data/system/locksettings.db里。对 user 0 即主用户来说,全新刷机首次开机时 credential 本来就是 NONE,所以“默认不设安全锁”这件事系统默认就满足,不需要额外改。
难点在于两点:第一,如何让 Keyguard 不显示 Swipe;第二,如何防止用户自己再去设置安全锁屏。
防止用户设置安全锁屏,通常有三种做法:在设置应用里隐藏对应菜单项;用 DevicePolicyManager 把密码质量锁在PASSWORD_QUALITY_UNSPECIFIED;或者在 LockSettingsService 的接口层把 setLockPattern、setPin、setPassword 这些调用拦掉。如果是商用设备,用设备管理策略最规范,但需要有一个激活的 DeviceAdmin,首次开机流程会比较绕。
2.3 Keyguard 是否显示的判断链
我把相关组件列一下,方便你在源码里定位:
- KeyguardViewMediator(SystemUI):决定 Keyguard 显示与隐藏,是核心调度器。
- KeyguardController(SystemUI):处理 Activity 与 Keyguard 之间的协调,比如锁定状态下是否允许某些界面覆盖。
- WindowManagerService 的 disableKeyguard:比较老的禁用接口,逐渐被替代,但有些定制系统还在用。
- LockPatternUtils 的 setLockScreenDisabled:显式禁用锁屏的开关。
判断链大致就是:用户是否显式禁用 + 是否有安全凭据 + devicePolicy 是否强制显示 + 当前是否处于锁屏状态。这四条合起来决定 Keyguard 最终出不出来。
所以要让默认无锁屏,最直接的钩子是在 SystemUI 启动后立即调用一次 dismiss,同时在 DevicePolicy 或者 LockSettingsService 层面禁止安全凭据的设置。只看一个开关是不行的。
2.4 选型建议
我在做方案选型时会把下面三种方式放在一起对比:
| 方案 | 改动位置 | 效果 | 风险 |
|---|---|---|---|
| 改 defaults.xml 中 lockscreen.disabled | SettingsProvider | 设置项默认显示为无,部分版本可隐藏 swipe | 不彻底,Android14 上经常只影响设置界面 |
| SystemServer 启动时调用 setLockScreenDisabled | SystemServer / LockSettingsService | 显式禁用锁屏,效果彻底 | 需要选好调用时机,要考虑 SELinux 权限 |
| 设备管理策略 DPM | DevicePolicyManager | 最规范,防止用户反设置 | 首次开机需要激活 admin,流程复杂 |
我最终推荐的是组合方案:先改 defaults.xml 让设置项和数据库默认值走到“无”,再在 SystemServer 侧调一次LockPatternUtils.setLockScreenDisabled(true, UserHandle.USER_SYSTEM)保证 Keyguard 真的被禁用。这样既满足设置界面的默认值,又能保证亮屏后直接进桌面。
3. 实操:把默认解锁方式改成“无”并完成编译
3.1 修改 SettingsProvider 的默认值 XML
以 overlay 方式修改 defaults.xml,内容大概是这样:
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <setting id="lockscreen.disabled" value="1"/> <setting id="lock_screen_lock_after_timeout" value="0"/> </resources>注意,不同 Android14 分支里lockscreen.disabled这个 id 是否存在,要以你拉取的源码为准。如果编译时提示资源未定义,说明这个字段在当前分支已经被移除或改名了,那就别在 XML 里强改,直接用下面的 SystemServer 方式更稳妥。
还有一个容易踩的细节:defaults.xml 里的这些值只在 SettingsProvider 首次创建数据库时生效。如果你刷机时保留了 userdata,settings_secure.xml里已经有旧值,默认值不会覆盖存量数据。所以刷机验证第一步要 wipe data,或者用adb shell settings delete secure lockscreen.disabled把旧值清掉再观察。
3.2 从 SystemServer 侧禁用 Keyguard
这一步是为了“撤掉闸机”。以 Android14 AOSP 源码为例,可以在 SystemServer 的startOtherServices阶段,等 LockSettingsService 初始化完成之后,直接调用:
LockPatternUtils lpu = new LockPatternUtils(context); lpu.setLockScreenDisabled(true, UserHandle.USER_SYSTEM);如果你更愿意直接操作设置项,也可以这样:
Settings.Secure.putIntForUser( context.getContentResolver(), Settings.Secure.LOCKSCREEN_DISABLED, 1, UserHandle.USER_SYSTEM );不过我更推荐前者,因为setLockScreenDisabled走的是 LockSettingsService 的逻辑,会和凭据状态、设备策略联动,而不是单纯写一个设置字段。
这里有一个非常关键的坑:调用时机必须在 Keyguard 第一次显示之前。如果在 SystemUI 已经启动、用户已经看到锁屏之后再调用,虽然最终会 dismiss,但屏幕点亮瞬间会闪一下锁屏,体验不好。我一般在startOtherServices里、SystemUI启动的onBootPhase之前执行,这样整个开机过程都不会出现 Keyguard。
另外,如果设备上还有其他定制的 App 在开机后主动触发锁屏,比如某些 Launcher 会调用DevicePolicyManager.lockNow(),那这一层也要排查。我曾经遇到过客户反馈“还是会锁屏”,最后发现是他们自己的推送 App 里写了防误触逻辑,把屏幕关了顺便锁了。
3.3 增量编译与刷机验证
修改完后,编译指令建议按需拆开:
source build/envsetup.sh lunch <你的产品> m SettingsProvider SystemUI services framework如果你只改了 defaults.xml,那么只编 SettingsProvider 和 SystemUI 就行;如果改了 SystemServer 逻辑,必须带上 framework 和 services。为了保险,我第一次跑的是全量m,因为 Android14 的模块依赖比较重,增量模块偶尔会漏掉资源索引更新。
编译完成后,如果跑模拟器可以make systemimage然后重启模拟器;真机则要把对应分区镜像刷进去。我习惯先刷一个新的 userdata,确保 defaults.xml 的导入逻辑从头走一遍。
验证命令我常用这三条:
adb shell settings get secure lockscreen.disabled adb shell locksettings get-disabled adb shell dumpsys window policy | grep -i keyguard第一条看数据库值是否被正确导入,第二条看 LockSettingsService 里的禁用标志,第三条看运行时的 Keyguard 显示状态。如果三条都对,重启、灭屏、亮屏,就能看到直接进桌面、锁屏不再出现。
3.4 参数选择和“为什么”
有朋友问,为什么不直接在frameworks/base/packages/SettingsProvider/res/values/defaults.xml里改,而是要折腾 overlay?因为直接改主源码的后果是:只要 AOSP 同步一次,这个改动就被覆盖;而且如果系统里维护了多款产品,不同产品可能需要不同的默认值,overlay 可以做到按 product 隔离。
为什么必须选 user 0?因为 Android14 的多用户体系里,每个用户都有自己的锁屏设置和凭据。开机后默认只有 user 0,你改了 user 10 也不会对首用户生效。所以所有默认值操作都锁定UserHandle.USER_SYSTEM。
为什么编译一定要带 SystemUI?因为 Keyguard 是 SystemUI 的一部分。历史上很多人只编 framework,结果刷进去 SettingsProvider 的值变了,但 SystemUI 还是旧的 KeyguardController 逻辑,自然没效果。
4. 编译报错 out/soong/build.ninja failed:一次完整的排查实录
4.1 报错现场还原
修改锁屏默认值的过程中,我很自然地撞上了那个经典编译错误。执行m SettingsProvider时,控制台刷出类似这样的信息:
FAILED: out/soong/build.ninja cd "$(dirname "out/soong/build.ninja")" && out/host/linux-x86/bin/soong_ui ...这种报错的诡异之处在于:它发生在 Soong 想重新生成构建图的时候,而不是发生在具体编译某个模块的时候。也就是说,可能是上一轮编译的中间状态坏了,也可能是这次改动引入了 Soong 解析不了的东西。
我在这个报错上花过很长时间,后来总结出的结论是:代码报错通常藏在更前面的日志里,最后一行的out/soong/build.ninja failed只是“果”不是“因”。你往上翻日志,往往会看到某个 Blueprint 文件语法错误、某个模块重复定义、或者磁盘写入失败之类的根因。
4.2 根因分析与排查顺序
我把常见原因和处理方式整理成了一张表,方便你对照:
| 现象 | 根因 | 处理方式 |
|---|---|---|
| 之前 Ctrl+C 中断过编译,重新编就报这个错 | out/soong 残留损坏 | 删除 out/soong 后重新编译 |
| 磁盘空间只剩几个 GB | 生成 build.ninja 时文件写不完整 | 清理 out 下旧镜像、ccache 缓存,或者加磁盘 |
| 最近改过 Android.bp / Android.mk | Soong 解析失败 | 翻日志找到具体模块,修正语法再编译 |
| 换过 JDK 或环境变量异常 | 工具链不匹配 | source build/envsetup.sh 重新初始化环境 |
| -j 并发太大导致 OOM | 编译进程被杀,ninja 文件生成一半 | 降低并发数,比如 -j8 或 -j4 |
很多时候,最直接的办法不是去修那个报错本身,而是把 out/soong 删掉重新生成。这个目录承担了 Soong 的所有中间状态,一坏就各种奇怪。
4.3 恢复操作与避坑
我现在的标准恢复序列是这样的:
cd $ANDROID_BUILD_TOP df -h # 先确认磁盘是否充足 rm -rf out/soong source build/envsetup.sh lunch <你的产品> m SettingsProvider 2>&1 | tee build.log如果删掉 out/soong 之后还是失败,我才会考虑rm -rf out/host/linux-x86或者整个rm -rf out。但整个 out 删掉意味着之前所有编译产物全部失效,重新构建一个完整系统可能要半小时到几小时,代价不小。所以这个操作要往后放。
还有一个比较少人提的坑:如果你之前用m和make混着执行,比如一会儿make framework,一会儿m SettingsProvider,Soong 的内部状态会出现不一致,也容易触发 build.ninja 重建失败。我在 Android14 上已经统一只用m,很少再碰make。
5. 常见问题与避坑清单(掉进去过的人才知道)
5.1 刷机后没生效?先查这几个地方
改了默认值之后刷机没效果,是最容易让人原地崩溃的情况。按我的排查顺序来:
先看自己是否保留了 userdata。首次开机时 SettingsProvider 才会导入 defaults.xml,如果设备是从旧状态升级的,settings_secure.xml里的旧值会优先,新默认值根本进不去。这个堪称第一大坑。
再看 overlay 有没有真正进入系统。你可以解包 system.img,找到SettingsProvider.apk里的资源,确认lockscreen.disabled的值是不是 1。很多时候产品配置里忘了加 overlay 目录,源码改了但产物没变。
再看签名和 SELinux。如果 SystemServer 侧调用setLockScreenDisabled时被 SELinux 拦截,logcat 里会有 avc denial 日志,但表面现象只是锁屏依然存在,没有任何 crash。我遇到过类似问题,排查半天最后发现是 sepolicy 没补规则。
5.2 用 adb 快速验证运行时状态
有时候改代码、编译、刷机这个循环太慢,我习惯先用 adb 命令验证思路是否正确:
adb shell locksettings set-disabled true adb shell locksettings get-disabledlocksettings set-disabled true执行之后,如果系统立刻不显示锁屏了,说明你的应用层思路没问题;如果执行完锁屏还在,那就是框架层的 Keyguard 判断链还有别的因素在干扰,比如 DevicePolicy 强制要求显示锁屏。
这种运行时验证非常关键,它能帮你把“代码没写对”和“机制理解错”区分开。我在很多项目里都是先用这一条命令做可行性验证,再回头改默认值。
5.3 三个容易忽略的坑
第一个坑是 OTA 升级不清 userdata。很多商用设备会走 OTA 升级,而 OTA 不会重置用户数据,所以 defaults.xml 的新默认值对存量设备不会生效。如果你是给已有产品做升级,必须考虑通过系统启动时的迁移逻辑去纠正已有设置,而不是指望默认值。
第二个坑是多个用户场景。有些设备开了多用户或者访客模式,user 0 改了无锁屏,但访客用户可能是另一个锁屏状态。虽然商用机大多不启用多用户,但只要系统里有其他用户,测试时就要用adb shell am switch-user切过去验证。
第三个坑是设置里“无”与“滑动”的混淆。在设置 App 的屏幕锁定界面里,“无”和“滑动”是两个不同的选项,它们底层走的代码路径不一样。默认选“无”要注意同时把锁定凭据清空,否则可能出现设置界面显示“无”,但一亮屏仍然要求滑动的情况。
5.4 避坑清单速查表
| 现象 | 原因 | 处理 |
|---|---|---|
| 改了 defaults.xml 没生效 | userdata 残留旧值 | 刷机时 wipe data,或用 adb 删除设置项 |
| 锁屏还有 Swipe 层 | 只改了设置默认值,没禁用 Keyguard | 补上 setLockScreenDisabled(true) 或 DPM 策略 |
| 编译报 out/soong/build.ninja failed | soong 中间状态损坏或磁盘不足 | 删除 out/soong,检查磁盘后重编 |
| 亮屏瞬间闪一下锁屏 | 禁用调用时机太晚 | 把逻辑放到 SystemServer 早期,SystemUI 启动之前 |
| 用户仍能设置密码 | 没有在设置层或 DPM 层拦截 | 隐藏设置入口,或使用 DevicePolicyManager 锁死密码质量 |
结尾
这次做完之后,我最深的体会是:做默认锁屏这类定制,前期把“无锁屏”的具体表现定义清楚,比后期调试省力得多。只改一个字段的“默认值”和真正让用户感知“没有锁屏”,本质是两件事,中间隔着 Keyguard 的整套判断链。
如果你也在做 Android14 的默认锁屏定制,建议开局先跑一条adb shell locksettings set-disabled true做可行性验证,再动源码和编译流程。这样能把“改代码”和“验证机制”分开排查,避免在编译报错和需求理解之间来回转圈。