16、Settings Provider:读取系统设置、写入系统设置、全局与安全设置

说实话,Settings Provider 可能是我们日常开发中最常用、却又最容易被忽视的 Content Provider。你想想看,从读取屏幕亮度、获取飞行模式状态,到判断当前时区,这些操作背后几乎都离不开它。

我个人习惯把 Settings Provider 看作是 Android 系统的「控制面板数据库」。它不像我们自定义的 Provider 那样需要自己写 URI 匹配、自己管理数据源——Google 已经帮我们封装好了。但正因为太方便,很多人反而忽略了它的权限模型和版本差异。

核心要点:Settings Provider 分为三类——System(系统设置)、Secure(安全设置)、Global(全局设置)。它们的读写权限和适用范围各不相同。

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,你的应用直接崩溃,那多尴尬。

小技巧:对于 String 类型的设置,用 getString() 更安全,因为它返回 null 而不是抛异常。但记得判空。

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 跳转到授权页,转化率才提上来。

注意:Secure 和 Global 的写入,普通应用基本别想了。除非你的应用是系统应用,或者有系统签名。否则调用 putInt() 会直接抛 SecurityException。

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 的核心逻辑:

Settings Provider 知识体系 Settings Provider System 系统设置 Secure 安全设置 Global 全局设置 屏幕亮度、音量、字体 写入需 WRITE_SETTINGS 锁屏、输入法、无障碍 普通应用不可写入 飞行模式、时区、漫游 普通应用不可写入 读取:普通权限即可 写入:System 需特殊权限,Secure/Global 仅系统应用

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