16、多用户与账户系统:多用户切换,Guest 模式,账户同步与个性化设置

车机不是一个人的私人物品。家里几个人开同一辆车,或者朋友临时借用,这种情况太常见了。Android Automotive OS 的多用户系统,说白了就是让每个人上车后,车机自动变成「自己的车」。

我最早接触这个模块时,觉得不就是手机上的多用户嘛,搬过来就行。结果踩了不少坑。车载场景和手机完全不同——用户切换时不能黑屏、导航不能断、音乐不能停。嗯,这里面的门道,我们一个一个说。

16.1 多用户架构:UserManager 与 UserHandle

Android 的多用户核心是 UserManager。每个用户都有一个唯一的 UserHandle,系统通过它来隔离数据、权限和进程。

在 Automotive 上,用户类型分这么几种:

用户类型 UserHandle 范围 说明
系统用户 USER_SYSTEM (0) 系统级服务运行在此用户,不可删除
主要用户 10 车主或主要驾驶员,拥有完整权限
次要用户 11+ 家庭成员或常用驾驶员
访客用户 USER_CURRENT_OR_SELF 临时使用,退出时数据可清除

我习惯在代码里这样判断当前用户类型:

UserManager um = context.getSystemService(UserManager.class);
UserHandle currentUser = Process.myUserHandle();

if (um.isSystemUser()) {
    // 系统用户,通常只有系统服务在这里跑
} else if (um.isGuestUser()) {
    // 访客模式,限制写入操作
} else {
    // 普通用户
}

这里有个坑。我曾经以为 isSystemUser() 返回 true 就代表是车主,其实不是。系统用户是 uid 为 0 的那个,普通应用根本跑不到那个用户空间里。车主的用户 ID 通常是 10,你得用 getSerialNumberForUser() 来确认。

16.2 多用户切换流程

用户切换不是简单的「登出再登入」。车机要求切换过程不能中断正在进行的服务,尤其是导航和媒体。

切换的核心流程是这样的:

// 发起切换
UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
UserHandle targetUser = new UserHandle(11);
um.switchUser(targetUser);

// 系统内部会依次触发:
// 1. 当前用户 onUserSwitching(switchingTo)
// 2. 目标用户 onUserStarting()
// 3. 切换完成回调

我个人建议,在 onUserSwitching 回调里,不要做耗时操作。我曾经试过在切换时同步大量联系人数据,结果切换动画卡了 3 秒,被测试同学提了 bug。后来改成异步 + 进度提示,体验好很多。

关键点: 切换用户时,SystemUI 和 Launcher 会重新启动。但 CarService 和一些核心系统服务会保持运行,只是切换到新用户的上下文中。

16.3 Guest 模式的实现细节

Guest 模式,说白了就是「用完即走」。朋友借车开一下,不想让他看到你的导航历史、通话记录、Spotify 歌单。

创建访客用户的代码很简单:

UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
UserHandle guest = um.createGuest(context, "访客");

// 设置访客退出时清除数据
um.setUserEphemeral(guest, true);

注意 setUserEphemeral 这个调用。它标记用户为「临时用户」,退出时系统会自动删除该用户的所有数据。但有个细节——如果访客在开车途中熄火锁车,下次启动时访客数据还在吗?

答案是:取决于实现。AOSP 默认行为是关机时不清除,只有显式「退出访客模式」时才清除。我参与的一个项目中,客户要求「熄火即清除」,我们不得不在 CarPowerManager 的关机回调里手动调用 removeUser()

注意: 访客模式下,不要允许安装应用或修改系统设置。否则清除数据时会出现残留文件。我曾经排查过一个 bug,访客安装了一个输入法,退出后输入法进程还在跑,因为它的 apk 文件被系统服务持有了句柄。

16.4 账户同步机制

每个用户登录自己的 Google 账户(或第三方账户),然后系统自动同步联系人、日历、应用数据。这个机制在手机上很成熟,但车机上有个特殊问题——多用户同时登录时,账户同步不能互相干扰。

Android 的账户同步由 AccountManagerSyncAdapter 配合完成。每个用户有自己的 AccountManagerService 实例,数据完全隔离。

// 获取当前用户的 AccountManager
AccountManager am = AccountManager.get(context);
Account[] accounts = am.getAccounts();

// 触发同步
ContentResolver.requestSync(account, ContactsContract.AUTHORITY, new Bundle());

我遇到过一个问题:用户 A 登录了账户,用户 B 切换过来后,AccountManager.get(context) 返回的是空列表。这是因为每个用户的账户数据存储在各自的 /data/system/users/{userId}/accounts.db 中。切换用户时,系统会自动切换数据库文件。

但有个坑——如果你在后台服务里缓存了 AccountManager 实例,切换用户后这个实例可能指向旧用户的上下文。我曾经在 CarService 里缓存了 AccountManager,结果用户 B 看到的却是用户 A 的账户列表。解决方案是每次使用时都重新获取:

// 不要这样
private AccountManager mAm;

// 应该这样
private AccountManager getAccountManager() {
    return AccountManager.get(getContextForUser());
}

16.5 个性化设置:每个用户都有自己的「车」

个性化设置是多用户系统最直观的价值。座椅位置、后视镜角度、空调温度、桌面布局、应用列表——每个用户都应该有自己的配置。

Android Automotive 使用 Settings.SystemSettings.Secure 来存储用户级别的设置。这些表在底层是按用户 ID 分区的:

// 写入当前用户的设置
Settings.System.putInt(getContentResolver(), "seat_position", 5);

// 读取时自动切换到当前用户
int pos = Settings.System.getInt(getContentResolver(), "seat_position", 0);

对于更复杂的个性化数据(比如桌面图标排列、快捷控制面板布局),我建议使用 SharedPreferences 加上用户 ID 作为前缀,或者直接使用 Room 数据库,在表中加一个 user_id 字段。

我曾经在一个项目中,把个性化数据全部存在 /data/data/{package}/shared_prefs/ 下。结果发现用户切换后,应用读取的还是旧数据。原因很简单——Context 对象没有切换。每个用户启动的应用进程是独立的,但如果你用了 getApplicationContext(),它可能指向的是系统用户上下文。

经验之谈: 在 Automotive 上,永远不要假设 Context 属于当前前台用户。建议在每次处理用户相关操作时,显式调用 context.createContextAsUser(user, 0) 来获取正确的上下文。

16.6 多用户下的进程与权限隔离

每个用户的应用运行在独立的进程中,uid 不同,文件系统权限也不同。这意味着用户 A 安装的微信,用户 B 看不到,也打不开。

但有些系统服务需要跨用户访问。比如 CarService 需要读取所有用户的驾驶偏好。这时候要用 INTERACT_ACROSS_USERS 权限:

<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />

这个权限是 signature 级别的,只有系统应用才能申请。普通第三方应用无法跨用户操作,这是安全设计的基本要求。

我记得有一次,一个第三方地图应用想在用户切换后继续播放导航语音。它用了 startService() 但没指定用户,结果服务跑在了旧用户空间里,新用户无法绑定。后来我们改成了 startServiceAsUser(),并让服务运行在系统用户下,才解决了这个问题。

16.7 多用户切换的 UI 与动画

车机上的用户切换,不能像手机那样直接锁屏。驾驶员正在开车,切换过程必须平滑、快速、不干扰驾驶。

Android Automotive 的默认实现是:点击用户头像 -> 弹出确认对话框 -> 显示加载动画 -> 切换到新用户的桌面。整个过程大约 1-2 秒。

我建议在切换动画期间,保持导航和媒体播放不中断。具体做法是:

  • 导航服务运行在系统用户进程中,不随用户切换而重启
  • 媒体播放器使用 MediaSession,跨用户保持连接
  • 切换时只更新 SystemUI 和 Launcher,不重启核心服务

嗯,这里要注意。如果你在切换时调用了 ActivityManager.killBackgroundProcesses(),可能会误杀正在播放音乐的应用。我踩过这个坑,后来改成只杀非前台、非媒体类的进程。

16.8 知识体系总览

下面这张图概括了多用户与账户系统的核心模块和交互关系:

多用户与账户系统架构 UserManager AccountManager Settings Provider 用户类型 系统用户 | 主要用户 次要用户 | 访客用户 账户同步 SyncAdapter ContentResolver.requestSync() 个性化设置 座椅/后视镜/空调 桌面布局/应用列表 用户切换流程 onUserSwitching onUserStarting 进程隔离 独立 uid / 独立数据目录 INTERACT_ACROSS_USERS Guest 模式 createGuest() setUserEphemeral() 每个用户拥有独立的账户、设置、应用数据,切换时核心服务保持运行

这张图展示了从用户管理到账户同步、个性化设置,再到底层切换流程和隔离机制的完整链路。每个模块之间通过 Binder 调用和 ContentProvider 进行通信,数据按用户 ID 严格分区。

多用户系统是 Android Automotive 区别于手机 Android 的重要特征之一。它不仅仅是「加几个用户账号」那么简单,而是涉及到进程管理、数据隔离、权限控制、UI 切换等一系列系统工程问题。理解了这个模块,你基本上就掌握了车机系统的一半精髓。