16、设置与系统偏好:Settings Panel、CarSetting、系统偏好存储、用户配置管理

说实话,做车载系统开发这么多年,我见过太多团队在「设置」这个模块上栽跟头。有人觉得不就是个开关列表嘛,随便写写就行。结果呢?用户配置一多,存储乱套,多用户切换时数据打架,最后不得不返工重写。

今天我们就来聊聊 Android Automotive OS 里的设置体系。我会把我在项目中踩过的坑、总结的经验,都摊开来讲。

16.1 设置的整体架构

Android Automotive 的设置,说白了分三层:

  • Settings Panel:系统级快捷面板,下拉就能调亮度、音量、WiFi 开关
  • CarSetting:深度设置应用,类似手机里的「设置」App
  • 系统偏好存储:底层数据怎么存、怎么读

这三层的关系,我画了张图,你看一眼就明白了。

Android Automotive OS 设置体系架构 Settings Panel(快捷面板) 亮度 / 音量 / WiFi / 蓝牙快捷开关 CarSetting(深度设置应用) 显示 / 声音 / 网络 / 账户 / 车辆设置 系统偏好存储 SharedPreferences / SettingsProvider / 用户配置管理 多用户隔离 / 数据持久化 / 配置同步

16.2 Settings Panel:快捷面板的秘密

Settings Panel 是用户最先接触到的设置入口。下拉状态栏,那些开关按钮就是它。

我记得第一次看 Automotive 的 Settings Panel 代码时,发现它跟手机上的完全不一样。手机上是 SystemUI 的一部分,车载上却是独立的 Service。

核心类就两个:

  • SettingsPanelService:管理面板的显示与隐藏
  • SettingsPanelView:实际的 UI 布局

启动方式很简单:

// 显示快捷面板
Intent intent = new Intent(SettingsPanelService.ACTION_SHOW_PANEL);
intent.putExtra(SettingsPanelService.EXTRA_PANEL_TYPE, 
                SettingsPanelService.PANEL_TYPE_QUICK_SETTINGS);
startService(intent);
我的经验: 别在 Settings Panel 里放太多开关。车载屏幕虽然大,但司机操作时间很短。我一般控制在 6-8 个核心开关,再多就放到 CarSetting 里。

16.3 CarSetting:深度设置应用

CarSetting 才是真正的设置主战场。它继承自 SettingsActivity,但做了大量车载定制。

你打开 CarSetting,看到的每个分类其实都是一个 PreferenceFragment。比如:

Fragment 类名 对应设置项
DisplaySettingsFragment 亮度、字体大小、深色模式
SoundSettingsFragment 音量、均衡器、音效
NetworkSettingsFragment WiFi、蓝牙、移动网络
AccountSettingsFragment 用户账户、同步
VehicleSettingsFragment 车门锁、灯光、空调

每个 Fragment 里用 PreferenceScreen 组织 UI。举个例子:

public class DisplaySettingsFragment extends PreferenceFragment {
    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        setPreferencesFromResource(R.xml.display_preferences, rootKey);
        
        // 亮度调节
        SeekBarPreference brightnessPref = findPreference("brightness");
        brightnessPref.setOnPreferenceChangeListener((pref, newValue) -> {
            int brightness = (int) newValue;
            // 写入系统设置
            Settings.System.putInt(getContentResolver(),
                Settings.System.SCREEN_BRIGHTNESS, brightness);
            return true;
        });
    }
}
注意: 车载的亮度调节跟手机不一样。手机只调屏幕亮度,车载还要考虑仪表盘亮度、氛围灯亮度。我曾经在项目里忘了处理氛围灯联动,结果用户调亮度时,氛围灯忽明忽暗,被测试提了 Bug。

16.4 系统偏好存储:数据到底放哪?

设置数据存哪里?这是个老生常谈的问题。Android Automotive 提供了三种方式:

  1. SharedPreferences:适合小量、应用私有的配置
  2. SettingsProvider:系统级设置,跨应用共享
  3. CarPropertyManager:车辆硬件属性,比如车门状态、空调温度

我个人的习惯是:

  • 用户偏好(比如主题、字体大小)→ SharedPreferences
  • 系统级开关(WiFi、蓝牙)→ Settings.GlobalSettings.System
  • 车辆硬件状态 → CarPropertyManager

举个例子,保存用户选择的深色模式:

// 保存
SharedPreferences prefs = getSharedPreferences("user_prefs", Context.MODE_PRIVATE);
prefs.edit().putBoolean("dark_mode", true).apply();

// 读取
boolean isDarkMode = prefs.getBoolean("dark_mode", false);

如果是系统级设置,用 SettingsProvider:

// 写入
Settings.Global.putInt(getContentResolver(), 
    "car_dark_mode", 1);

// 读取
int value = Settings.Global.getInt(getContentResolver(),
    "car_dark_mode", 0);
核心原则: 应用私有数据用 SharedPreferences,系统共享数据用 SettingsProvider。别混用,否则多用户切换时会出大问题。

16.5 用户配置管理:多用户场景的坑

车载系统跟手机最大的不同,就是多用户。一辆车可能被家庭多人使用,每个人都有自己的设置偏好。

Android Automotive 用 UserManager 管理用户。每个用户有自己的数据目录,SharedPreferences 天然隔离。但 SettingsProvider 是全局的,需要手动处理。

我曾经在项目里遇到一个问题:用户 A 调整了座椅位置,用户 B 上车后座椅还是 A 的位置。原因就是座椅数据存到了全局 SettingsProvider,没有按用户隔离。

解决方案是这样的:

// 获取当前用户
UserHandle user = Process.myUserHandle();

// 按用户存储
String key = "seat_position_" + user.getIdentifier();
Settings.System.putInt(getContentResolver(), key, position);

// 读取时也按用户
int savedPosition = Settings.System.getInt(getContentResolver(),
    "seat_position_" + user.getIdentifier(), 0);

更规范的做法是使用 Settings.SystemputStringForUser 方法:

Settings.System.putStringForUser(
    getContentResolver(),
    "seat_position",
    String.valueOf(position),
    user.getIdentifier());
避坑指南: 我曾经在项目里直接用 Settings.Global 存用户偏好,结果用户切换后数据全乱了。后来改成 Settings.System + 按用户隔离,才解决问题。记住:Settings.Global 是所有用户共享的,Settings.System 可以按用户隔离。

16.6 实战:实现一个用户配置切换

说了这么多,我们来写个实际例子。假设我们要实现用户切换时,自动加载该用户的座椅位置和空调温度。

public class UserConfigManager {
    private Context context;
    private ContentResolver resolver;
    
    public UserConfigManager(Context context) {
        this.context = context;
        this.resolver = context.getContentResolver();
    }
    
    // 保存当前用户配置
    public void saveUserConfig(UserHandle user, int seatPos, int acTemp) {
        Settings.System.putStringForUser(resolver, 
            "seat_position", String.valueOf(seatPos), user.getIdentifier());
        Settings.System.putStringForUser(resolver,
            "ac_temperature", String.valueOf(acTemp), user.getIdentifier());
    }
    
    // 加载指定用户配置
    public UserConfig loadUserConfig(UserHandle user) {
        String seatStr = Settings.System.getStringForUser(resolver,
            "seat_position", user.getIdentifier());
        String acStr = Settings.System.getStringForUser(resolver,
            "ac_temperature", user.getIdentifier());
            
        UserConfig config = new UserConfig();
        config.seatPosition = seatStr != null ? Integer.parseInt(seatStr) : 0;
        config.acTemperature = acStr != null ? Integer.parseInt(acStr) : 24;
        return config;
    }
    
    static class UserConfig {
        int seatPosition;
        int acTemperature;
    }
}

当用户切换时,监听 ACTION_USER_SWITCHED 广播:

BroadcastReceiver userSwitchReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) {
            int newUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
            UserHandle newUser = UserHandle.of(newUserId);
            
            UserConfig config = userConfigManager.loadUserConfig(newUser);
            // 应用配置到车辆硬件
            applySeatPosition(config.seatPosition);
            applyAcTemperature(config.acTemperature);
        }
    }
};
小技巧: 用户切换时,最好加个过渡动画。直接切换座椅位置可能会夹到人。我一般先让座椅退到最后,再移动到目标位置,安全第一。

16.7 总结一下

设置与系统偏好这块,说白了就三件事:

  • 快捷面板给用户快速操作
  • CarSetting 提供深度配置
  • 存储层保证数据不丢、不乱

多用户场景下,一定要做好数据隔离。别问我怎么知道的,问就是我改过太多这种 Bug。

嗯,今天就到这里。代码都在上面了,你拿去用,有问题随时交流。


公众号:蓝海资料掘金营,微信 deep3321