16、Settings Provider:读取系统设置、写入系统设置、全局与安全设置
说实话,Settings Provider 可能是我们日常开发中最常用、却又最容易被忽视的 Content Provider。你想想看,从读取屏幕亮度、获取飞行模式状态,到判断当前时区,这些操作背后几乎都离不开它。
我个人习惯把 Settings Provider 看作是 Android 系统的「控制面板数据库」。它不像我们自定义的 Provider 那样需要自己写 URI 匹配、自己管理数据源——Google 已经帮我们封装好了。但正因为太方便,很多人反而忽略了它的权限模型和版本差异。
16.1 三类设置的定位与区别
我们先理清这三兄弟的关系。我用一个表格来对比,这样更直观:
| 类型 | 常量类 | 读写权限 | 典型用途 |
|---|---|---|---|
| System | Settings.System | 读:普通权限 写:WRITE_SETTINGS |
屏幕亮度、字体大小、音量 |
| Secure | Settings.Secure | 读:普通权限 写:系统级权限(普通应用不可写) |
锁屏方式、无障碍设置、输入法 |
| Global | Settings.Global | 读:普通权限 写:系统级权限 |
飞行模式、时区、数据漫游 |
嗯,这里要注意一点:Secure 和 Global 的写入权限,普通应用是拿不到的。我曾经在项目里试图通过 Settings.Secure.putInt() 去修改锁屏超时时间,结果直接抛了个 SecurityException。后来查文档才发现,这两个类从 Android 4.2 开始就收紧了权限。
16.2 读取系统设置:简单但别踩坑
读取操作相对简单,但有几个细节我建议你留意。先看代码:
// 读取屏幕亮度(System 类型)
int brightness = Settings.System.getInt(
getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS,
255 // 默认值
);
// 读取飞行模式状态(Global 类型)
int airplaneMode = Settings.Global.getInt(
getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON,
0
);
// 读取当前输入法(Secure 类型)
String defaultInputMethod = Settings.Secure.getString(
getContentResolver(),
Settings.Secure.DEFAULT_INPUT_METHOD
);
这里有个坑——getInt() 方法如果 key 不存在会抛出异常。所以我个人习惯总是传一个默认值进去,或者用 try-catch 包一下。你想想看,如果某个厂商定制 ROM 删掉了某个 key,你的应用直接崩溃,那多尴尬。
16.3 写入系统设置:权限是最大门槛
写入操作就没那么随意了。以修改屏幕亮度为例:
// 检查是否有 WRITE_SETTINGS 权限
if (Settings.System.canWrite(context)) {
// 有权限,直接写入
Settings.System.putInt(
getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS,
128
);
} else {
// 没有权限,需要引导用户去设置页授权
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
intent.setData(Uri.parse("package:" + context.getPackageName()));
startActivity(intent);
}
从 Android 6.0(API 23)开始,WRITE_SETTINGS 变成了特殊权限,不能像普通危险权限那样在运行时弹窗申请。用户必须手动去「设置 → 应用 → 特殊权限 → 修改系统设置」里开启。
我曾经在一个工具类 App 里遇到过这种情况:用户反馈说「为什么我调了亮度没反应?」排查了半天,发现是权限没申请。后来我在首次启动时弹了个引导对话框,配合 Intent 跳转到授权页,转化率才提上来。
16.4 全局与安全设置:只读场景居多
在实际开发中,我们读取 Global 和 Secure 的场景远多于写入。比如:
- 判断飞行模式:读取 Settings.Global.AIRPLANE_MODE_ON
- 获取当前时区:读取 Settings.Global.AUTO_TIME_ZONE
- 检查是否开启开发者选项:读取 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED
- 获取当前输入法列表:读取 Settings.Secure.ENABLED_INPUT_METHODS
这些信息对应用来说很有价值。比如我做的一个地图导航 App,就需要判断用户是否开启了「自动时区」。如果关闭了,我们就用 GPS 获取的时区来校准时间,否则直接用系统时间。
但写入这些值?嗯,除非你的应用是系统级工具,否则别想了。我记得有一次尝试用 adb 命令去修改 Secure 里的锁屏方式,结果提示「Permission denied」。后来查源码才发现,Secure 的 ContentProvider 在写入时会校验调用者的 UID 和签名。
16.5 知识体系结构图
下面这张图总结了 Settings Provider 的核心逻辑:
16.6 监听设置变化:ContentObserver 的妙用
有时候我们需要实时感知设置的变化。比如用户手动调了亮度,你的阅读类 App 需要同步调整主题。这时候可以用 ContentObserver:
getContentResolver().registerContentObserver(
Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS),
false,
new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
// 重新读取亮度值,更新 UI
int brightness = Settings.System.getInt(
getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS,
255
);
updateThemeByBrightness(brightness);
}
}
);
这里有个细节:第二个参数为 false,表示只监听精确匹配的 URI,不监听子路径。对于 Settings 来说,每个 key 对应一个独立的 URI,所以 false 就够了。
我个人习惯在 onResume() 里注册观察者,在 onPause() 里注销。这样既保证实时性,又避免内存泄漏。
16.7 版本兼容性提醒
最后说一个容易忽略的点:不同 Android 版本之间,某些 key 可能会迁移。比如 Android 4.2 之前,飞行模式存在 Settings.System 里,之后移到了 Settings.Global。如果你还在用旧 key 去读,返回的永远是默认值。
我的建议是:
- 优先使用官方文档中标注的常量,比如 Settings.Global.AIRPLANE_MODE_ON
- 如果某个 key 在新版本中废弃了,用 @Deprecated 注解标记的常量要谨慎
- 实在不确定时,可以同时读 System 和 Global,取非默认值那个
嗯,Settings Provider 的内容就这些。说白了,它就是一个系统级的键值对数据库,但权限模型和版本差异是最大的坑。只要记住「System 可写、Secure/Global 只读」这个原则,大部分场景都不会出问题。
公众号:蓝海资料掘金营,微信deep3321