UserPickerActivity 内部逻辑分析(android15.0.0_r17)
本文是作者基于AOSP15_r17的源码进行整理,摘录代码后由AI整理文章格式和相关图示。
已确认代码和图示的正确性,请参考。
适用版本:
android-15.0.0_r17模块路径:
packages/apps/Car/SystemUI核心类:
com.android.systemui.car.userpicker.UserPickerActivity、UserPickerController、UserEventManager、UserPickerAdapter、CarServiceMediator
1. 背景与定位
UserPickerActivity 是 AAOS(Android Automotive OS)Car SystemUI 中的全屏用户选择界面。它负责在指定 Display 上展示可登录用户列表,并支持以下能力:
- 在非 Driver Display 上关闭当前屏幕。
- 在用户已登录时提供退出登录能力。
- 展示当前 Display 已登录用户、其他座位已登录用户、正在停止的用户、不可用安全用户等状态。
- 支持启动访客会话。
- 支持新增用户入口。
- 监听用户生命周期事件,并实时刷新列表状态。
- 通过 Trampoline Pattern 确保 Activity 最终运行在 System User(user 0)下。
从功能职责上看,UserPickerActivity 更偏向 View 层初始化与生命周期管理;真正的用户状态判断、用户启动/停止、访客创建、Display/User 绑定关系判断等逻辑集中在 UserPickerController、UserEventManager 和 CarServiceMediator 中。
2. 相关文件与类
| 类型 | 路径 / 类 | 作用 |
|---|---|---|
| Manifest | packages/apps/Car/SystemUI/AndroidManifest.xml |
声明 Car SystemUI 中的 Activity 组件。 |
| Activity | UserPickerActivity.java |
用户选择界面入口,负责 UI 初始化、Controller 初始化、Dumpable 注册。 |
| Controller | UserPickerController.java |
用户列表构建、点击处理、登录/退出/访客/新增用户主逻辑。 |
| Event Manager | UserEventManager.java |
对接 UserManager、CarUserManager 用户生命周期事件,并通知 UI 刷新。 |
| Adapter | UserPickerAdapter.java |
RecyclerView 列表绑定与不同用户状态的视觉展示。 |
| Record | UserRecord.java |
用户卡片数据模型,包含图标、登录状态、座位信息、点击回调等。 |
| Shared State | UserPickerSharedState.java |
记录正在登录、正在停止的用户状态,避免多屏并发冲突。 |
| Car 中介 | CarServiceMediator.java |
封装 Occupant Zone、Display/User 映射、Display 电源控制等 Car Service 能力。 |
| Power | CarPowerManagementService.java、DisplayInterface.java、WakeLockInterface.java |
用户选择界面中"关闭屏幕"能力的下游实现。 |
3. Activity 启动与 Trampoline Pattern
UserPickerActivity 的 onCreate() 中首先判断当前 Activity 是否需要以 System User 身份启动:
java
@Override
protected void onCreate(Bundle savedInstanceState) {
if (shouldStartAsSystemUser() && !ActivityHelper.startUserPickerAsUserSystem(this)) {
super.onCreate(savedInstanceState);
return;
}
super.onCreate(savedInstanceState);
setShowWhenLocked(true);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getOnBackInvokedDispatcher()
.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mIgnoreBackCallback);
init();
}
关键点:
-
System User 执行约束
User Picker 需要跨 User/Display 做用户切换与状态查询,因此 Activity 采用 Trampoline Pattern:如果当前不是期望的 System User 执行环境,则重新以
UserHandle.SYSTEM启动自身,并结束当前实例。 -
锁屏可见
setShowWhenLocked(true)允许用户选择界面在锁屏场景下展示。 -
无标题全屏强化
requestWindowFeature(Window.FEATURE_NO_TITLE)移除标准标题栏。 -
屏蔽返回手势/返回键默认行为
注册
mIgnoreBackCallback后,返回事件不会直接退出,真正允许退出的场景由 Controller 或 UI 中的 Back Button 控制。
4. 初始化流程
4.1 init() 主流程
java
@VisibleForTesting
void init() {
mIsDriver = getIsDriver();
LayoutInflater inflater = LayoutInflater.from(this);
mRootView = inflater.inflate(R.layout.user_picker, null);
if (getWindow() != null) {
setContentView(mRootView);
initWindow();
}
initManagers(mRootView);
initViews();
initController();
mController.onConfigurationChanged();
String dumpableName = TAG + "#" + getDisplayId();
mDumpManager.unregisterDumpable(dumpableName);
mDumpManager.registerNormalDumpable(dumpableName, this);
}
初始化顺序非常重要:
#mermaid-svg-8WEzZeIF4pqPjA38{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8WEzZeIF4pqPjA38 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8WEzZeIF4pqPjA38 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8WEzZeIF4pqPjA38 .error-icon{fill:#552222;}#mermaid-svg-8WEzZeIF4pqPjA38 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8WEzZeIF4pqPjA38 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8WEzZeIF4pqPjA38 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8WEzZeIF4pqPjA38 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8WEzZeIF4pqPjA38 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8WEzZeIF4pqPjA38 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8WEzZeIF4pqPjA38 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8WEzZeIF4pqPjA38 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8WEzZeIF4pqPjA38 .marker.cross{stroke:#333333;}#mermaid-svg-8WEzZeIF4pqPjA38 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8WEzZeIF4pqPjA38 p{margin:0;}#mermaid-svg-8WEzZeIF4pqPjA38 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8WEzZeIF4pqPjA38 .cluster-label text{fill:#333;}#mermaid-svg-8WEzZeIF4pqPjA38 .cluster-label span{color:#333;}#mermaid-svg-8WEzZeIF4pqPjA38 .cluster-label span p{background-color:transparent;}#mermaid-svg-8WEzZeIF4pqPjA38 .label text,#mermaid-svg-8WEzZeIF4pqPjA38 span{fill:#333;color:#333;}#mermaid-svg-8WEzZeIF4pqPjA38 .node rect,#mermaid-svg-8WEzZeIF4pqPjA38 .node circle,#mermaid-svg-8WEzZeIF4pqPjA38 .node ellipse,#mermaid-svg-8WEzZeIF4pqPjA38 .node polygon,#mermaid-svg-8WEzZeIF4pqPjA38 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8WEzZeIF4pqPjA38 .rough-node .label text,#mermaid-svg-8WEzZeIF4pqPjA38 .node .label text,#mermaid-svg-8WEzZeIF4pqPjA38 .image-shape .label,#mermaid-svg-8WEzZeIF4pqPjA38 .icon-shape .label{text-anchor:middle;}#mermaid-svg-8WEzZeIF4pqPjA38 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8WEzZeIF4pqPjA38 .rough-node .label,#mermaid-svg-8WEzZeIF4pqPjA38 .node .label,#mermaid-svg-8WEzZeIF4pqPjA38 .image-shape .label,#mermaid-svg-8WEzZeIF4pqPjA38 .icon-shape .label{text-align:center;}#mermaid-svg-8WEzZeIF4pqPjA38 .node.clickable{cursor:pointer;}#mermaid-svg-8WEzZeIF4pqPjA38 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8WEzZeIF4pqPjA38 .arrowheadPath{fill:#333333;}#mermaid-svg-8WEzZeIF4pqPjA38 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8WEzZeIF4pqPjA38 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8WEzZeIF4pqPjA38 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8WEzZeIF4pqPjA38 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8WEzZeIF4pqPjA38 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8WEzZeIF4pqPjA38 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8WEzZeIF4pqPjA38 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8WEzZeIF4pqPjA38 .cluster text{fill:#333;}#mermaid-svg-8WEzZeIF4pqPjA38 .cluster span{color:#333;}#mermaid-svg-8WEzZeIF4pqPjA38 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8WEzZeIF4pqPjA38 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8WEzZeIF4pqPjA38 rect.text{fill:none;stroke-width:0;}#mermaid-svg-8WEzZeIF4pqPjA38 .icon-shape,#mermaid-svg-8WEzZeIF4pqPjA38 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8WEzZeIF4pqPjA38 .icon-shape p,#mermaid-svg-8WEzZeIF4pqPjA38 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8WEzZeIF4pqPjA38 .icon-shape .label rect,#mermaid-svg-8WEzZeIF4pqPjA38 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8WEzZeIF4pqPjA38 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8WEzZeIF4pqPjA38 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8WEzZeIF4pqPjA38 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} yes
no
onCreate
是否需要以 System User 启动
ActivityHelper.startUserPickerAsUserSystem
结束当前实例
super.onCreate
setShowWhenLocked / no title / back callback
init
getIsDriver
inflate R.layout.user_picker
setContentView + initWindow
initManagers
initViews
initController
onConfigurationChanged
updateTexts + updateUsers
register Dumpable
4.2 Window 初始化
initWindow() 主要处理窗口 Insets 与系统栏显示策略:
- 禁用由 Insets 导致的 relayout。
- 隐藏状态栏与导航栏。
- 让 User Picker 保持沉浸式展示。
这部分可以避免用户选择界面被 System Bar 挤压,保证车机多屏场景中界面布局稳定。
4.3 Manager 初始化
java
private void initManagers(View rootView) {
mDialogManager.initContextFromView(rootView);
mSnackbarManager.setRootView(rootView, R.id.user_picker_bottom_bar);
}
DialogManager:用于显示切换用户中、确认新增用户、达到最大用户数等对话框。SnackbarManager:用于底部提示,例如用户已在其他座位登录、用户正在退出、安全用户不可用等。
5. View 层结构与按钮逻辑
5.1 initViews()
java
private void initViews() {
View powerBtn = mRootView.findViewById(R.id.power_button_icon_view);
powerBtn.setOnClickListener(v -> mController.screenOffDisplay());
if (mIsDriver) {
powerBtn.setVisibility(View.GONE);
}
mHeaderBarTextForLogout = mRootView.findViewById(R.id.message);
mLogoutButton = mRootView.findViewById(R.id.logout_button_icon_view);
mLogoutButton.setOnClickListener(v -> mController.logoutUser());
mBackButton = mRootView.findViewById(R.id.back_button);
mBackButton.setOnClickListener(v -> finishAndRemoveTask());
initRecyclerView();
View bottomBar = mRootView.findViewById(R.id.user_picker_bottom_bar);
if (bottomBar instanceof ViewGroup) {
mCarSystemBarElementInitializer.initializeCarSystemBarElements((ViewGroup) bottomBar);
}
}
| UI 元素 | 逻辑 |
|---|---|
| Power Button | 非 Driver Display 可见;点击后调用 mController.screenOffDisplay()。 |
| Logout Button | 当前 Display 已登录时用于退出用户。 |
| Back Button | 当前 Display 已登录时可直接退出 User Picker 界面。 |
| RecyclerView | 展示用户卡片、访客、新增用户入口。 |
| Bottom Bar | 初始化 Car System Bar 元素,承载底部提示区域。 |
Driver Display 不显示 Power Button 的原因是底层 CarPowerManagementService 不允许普通外部调用关闭 Driver Display;服务端会校验 Driver Display 并抛出 UnsupportedOperationException。
6. RecyclerView 与 Adapter
6.1 RecyclerView 初始化
java
private void initRecyclerView() {
int numCols = getResources().getInteger(R.integer.user_fullscreen_switcher_num_col);
mUserPickerView = mRootView.findViewById(R.id.user_picker);
mUserPickerView.setLayoutManager(new GridLayoutManager(this, numCols));
mAdapter = createUserPickerAdapter();
mUserPickerView.setAdapter(mAdapter);
}
默认列数来自资源 R.integer.user_fullscreen_switcher_num_col,参考文档中记录为每行 3 个。
6.2 Adapter 绑定逻辑
java
@Override
public void onBindViewHolder(@NonNull UserPickerAdapterViewHolder holder, int position) {
setItemSpacing(holder.mView, position);
UserRecord userRecord = mUsers.get(position);
holder.mUserAvatarImageView.setImageDrawable(userRecord.mIcon);
holder.mFrame.setBackgroundResource(0);
holder.mUserNameTextView.setText(userRecord.mName);
setUserLoggedInInfo(holder, userRecord);
holder.mView.setOnClickListener(userRecord.mOnClickListener);
}
Adapter 不负责判断用户能否启动,它只根据 UserRecord 的状态展示 UI,并把点击事件交给 UserRecord.mOnClickListener。
6.3 用户状态展示规则
#mermaid-svg-LedydPJxIJUyZ72h{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LedydPJxIJUyZ72h .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LedydPJxIJUyZ72h .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LedydPJxIJUyZ72h .error-icon{fill:#552222;}#mermaid-svg-LedydPJxIJUyZ72h .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LedydPJxIJUyZ72h .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LedydPJxIJUyZ72h .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LedydPJxIJUyZ72h .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LedydPJxIJUyZ72h .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LedydPJxIJUyZ72h .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LedydPJxIJUyZ72h .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LedydPJxIJUyZ72h .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LedydPJxIJUyZ72h .marker.cross{stroke:#333333;}#mermaid-svg-LedydPJxIJUyZ72h svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LedydPJxIJUyZ72h p{margin:0;}#mermaid-svg-LedydPJxIJUyZ72h .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LedydPJxIJUyZ72h .cluster-label text{fill:#333;}#mermaid-svg-LedydPJxIJUyZ72h .cluster-label span{color:#333;}#mermaid-svg-LedydPJxIJUyZ72h .cluster-label span p{background-color:transparent;}#mermaid-svg-LedydPJxIJUyZ72h .label text,#mermaid-svg-LedydPJxIJUyZ72h span{fill:#333;color:#333;}#mermaid-svg-LedydPJxIJUyZ72h .node rect,#mermaid-svg-LedydPJxIJUyZ72h .node circle,#mermaid-svg-LedydPJxIJUyZ72h .node ellipse,#mermaid-svg-LedydPJxIJUyZ72h .node polygon,#mermaid-svg-LedydPJxIJUyZ72h .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LedydPJxIJUyZ72h .rough-node .label text,#mermaid-svg-LedydPJxIJUyZ72h .node .label text,#mermaid-svg-LedydPJxIJUyZ72h .image-shape .label,#mermaid-svg-LedydPJxIJUyZ72h .icon-shape .label{text-anchor:middle;}#mermaid-svg-LedydPJxIJUyZ72h .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LedydPJxIJUyZ72h .rough-node .label,#mermaid-svg-LedydPJxIJUyZ72h .node .label,#mermaid-svg-LedydPJxIJUyZ72h .image-shape .label,#mermaid-svg-LedydPJxIJUyZ72h .icon-shape .label{text-align:center;}#mermaid-svg-LedydPJxIJUyZ72h .node.clickable{cursor:pointer;}#mermaid-svg-LedydPJxIJUyZ72h .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LedydPJxIJUyZ72h .arrowheadPath{fill:#333333;}#mermaid-svg-LedydPJxIJUyZ72h .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LedydPJxIJUyZ72h .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LedydPJxIJUyZ72h .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LedydPJxIJUyZ72h .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LedydPJxIJUyZ72h .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LedydPJxIJUyZ72h .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LedydPJxIJUyZ72h .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LedydPJxIJUyZ72h .cluster text{fill:#333;}#mermaid-svg-LedydPJxIJUyZ72h .cluster span{color:#333;}#mermaid-svg-LedydPJxIJUyZ72h div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LedydPJxIJUyZ72h .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LedydPJxIJUyZ72h rect.text{fill:none;stroke-width:0;}#mermaid-svg-LedydPJxIJUyZ72h .icon-shape,#mermaid-svg-LedydPJxIJUyZ72h .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LedydPJxIJUyZ72h .icon-shape p,#mermaid-svg-LedydPJxIJUyZ72h .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LedydPJxIJUyZ72h .icon-shape .label rect,#mermaid-svg-LedydPJxIJUyZ72h .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LedydPJxIJUyZ72h .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LedydPJxIJUyZ72h .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LedydPJxIJUyZ72h :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} true
false
false
true
false
true
true
false
UserRecord
mIsStopping
显示 stopping 文案
置灰
不可点击效果
mIsLoggedIn
安全用户 + 非默认屏 + 不支持安全乘客用户
显示 unavailable secure user
置灰
普通未登录状态
不显示状态文字
mLoggedInDisplay == 当前 display
显示当前已登录
头像高亮边框
显示已在其他座位登录
置灰
关键展示逻辑:
java
private void setUserLoggedInInfo(UserPickerAdapterViewHolder holder, UserRecord userRecord) {
if (!userRecord.mIsStopping && !userRecord.mIsLoggedIn) {
if (userRecord.mIsSecure
&& mDisplayId != Display.DEFAULT_DISPLAY
&& !Flags.supportsSecurePassengerUsers()) {
holder.mUserBorderImageView.setVisibility(View.INVISIBLE);
holder.mLoggedInTextView.setText(mUnavailableSecureUserText);
updateAlpha(holder, true);
return;
}
holder.mUserBorderImageView.setVisibility(View.INVISIBLE);
holder.mLoggedInTextView.setText("");
updateAlpha(holder, false);
return;
}
if (userRecord.mIsStopping) {
holder.mUserBorderImageView.setVisibility(View.INVISIBLE);
holder.mLoggedInTextView.setText(mStoppingUserText);
holder.mLoggedInTextView.setTextColor(mOtherUserSubtitleColor);
updateAlpha(holder, true);
} else if (userRecord.mIsLoggedIn) {
if (userRecord.mLoggedInDisplay == mDisplayId) {
holder.mUserBorderImageView.setVisibility(View.VISIBLE);
holder.mLoggedInTextView.setText(mLoggedInText);
holder.mLoggedInTextView.setTextColor(mCurrentUserSubtitleColor);
updateAlpha(holder, false);
} else {
holder.mUserBorderImageView.setVisibility(View.INVISIBLE);
holder.mLoggedInTextView.setText(
String.format(mPrefixOtherSeatLoggedInInfo, userRecord.mSeatLocationName));
holder.mLoggedInTextView.setTextColor(mOtherUserSubtitleColor);
updateAlpha(holder, true);
}
}
}
7. Controller 初始化与回调链路
7.1 Activity 初始化 Controller
java
private void initController() {
mController.init(mCallbacks, getDisplayId());
}
private final Callbacks mCallbacks = new Callbacks() {
@Override
public void onUpdateUsers(List<UserRecord> users) {
mAdapter.updateUsers(users);
mAdapter.notifyDataSetChanged();
}
@Override
public void onHeaderStateChanged(HeaderState headerState) {
setupHeaderBar(headerState);
}
@Override
public void onFinishRequested() {
finishAndRemoveTask();
}
};
Activity 向 Controller 注入 UI 回调后,Controller 负责:
- 通知 Adapter 更新用户列表。
- 通知 Activity 切换 Header 显示状态。
- 请求 Activity 关闭页面。
7.2 Controller init()
java
void init(Callbacks callbacks, int displayId) {
mCallbacks = callbacks;
mDisplayId = displayId;
boolean isLoggedOutState = mCarServiceMediator.getUserForDisplay(mDisplayId)
== UserHandle.USER_NULL;
mHeaderState = new HeaderState(callbacks);
mHeaderState.setState(isLoggedOutState
? HEADER_STATE_LOGOUT
: HEADER_STATE_CHANGE_USER);
mUserEventManager.registerOnUpdateUsersListener(mUsersUpdateListener, mDisplayId);
}
Header 状态由当前 Display 是否已有绑定 User 决定:
| 条件 | Header 状态 | UI 含义 |
|---|---|---|
getUserForDisplay(displayId) == INVALID_USER_ID |
HEADER_STATE_LOGOUT |
当前屏幕未登录,展示登录选择态。 |
| 当前 Display 已有关联用户 | HEADER_STATE_CHANGE_USER |
当前屏幕已登录,展示切换/退出态。 |
8. 用户列表数据源与 UserRecord 构建
8.1 刷新入口
onConfigurationChanged() 会触发文案与用户列表刷新:
java
void onConfigurationChanged() {
updateTexts();
updateUsers();
}
void updateUsers() {
mCallbacks.onUpdateUsers(createUserRecords());
}
8.2 Alive Users 数据源
UserEventManager.getAliveUsers() 从 UserManager 获取 alive 用户列表,并过滤 System User 与 Guest:
java
List<UserInfo> getAliveUsers() {
List<UserInfo> aliveUsers = mUserManager.getAliveUsers();
for (int i = aliveUsers.size() - 1; i >= 0; i--) {
UserInfo userInfo = aliveUsers.get(i);
if ((isHeadlessSystemUserMode() && userInfo.id == USER_SYSTEM) || userInfo.isGuest()) {
aliveUsers.remove(i);
}
}
return aliveUsers;
}
Framework 中 UserManager.getAliveUsers() 实际等价于:
java
@TestApi
public @NonNull List<UserInfo> getAliveUsers() {
return getUsers(true, true, true);
}
参数含义:
excludePartial = trueexcludeDying = trueexcludePreCreated = true
因此 User Picker 展示的基础用户集合排除了 partial、dying、pre-created 用户,并且在 Headless System User Mode 下排除 System User。
8.3 createUserRecords() 主逻辑
#mermaid-svg-LS5aHkO4Y42CoKUj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LS5aHkO4Y42CoKUj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LS5aHkO4Y42CoKUj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LS5aHkO4Y42CoKUj .error-icon{fill:#552222;}#mermaid-svg-LS5aHkO4Y42CoKUj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LS5aHkO4Y42CoKUj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LS5aHkO4Y42CoKUj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LS5aHkO4Y42CoKUj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LS5aHkO4Y42CoKUj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LS5aHkO4Y42CoKUj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LS5aHkO4Y42CoKUj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LS5aHkO4Y42CoKUj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LS5aHkO4Y42CoKUj .marker.cross{stroke:#333333;}#mermaid-svg-LS5aHkO4Y42CoKUj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LS5aHkO4Y42CoKUj p{margin:0;}#mermaid-svg-LS5aHkO4Y42CoKUj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LS5aHkO4Y42CoKUj .cluster-label text{fill:#333;}#mermaid-svg-LS5aHkO4Y42CoKUj .cluster-label span{color:#333;}#mermaid-svg-LS5aHkO4Y42CoKUj .cluster-label span p{background-color:transparent;}#mermaid-svg-LS5aHkO4Y42CoKUj .label text,#mermaid-svg-LS5aHkO4Y42CoKUj span{fill:#333;color:#333;}#mermaid-svg-LS5aHkO4Y42CoKUj .node rect,#mermaid-svg-LS5aHkO4Y42CoKUj .node circle,#mermaid-svg-LS5aHkO4Y42CoKUj .node ellipse,#mermaid-svg-LS5aHkO4Y42CoKUj .node polygon,#mermaid-svg-LS5aHkO4Y42CoKUj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LS5aHkO4Y42CoKUj .rough-node .label text,#mermaid-svg-LS5aHkO4Y42CoKUj .node .label text,#mermaid-svg-LS5aHkO4Y42CoKUj .image-shape .label,#mermaid-svg-LS5aHkO4Y42CoKUj .icon-shape .label{text-anchor:middle;}#mermaid-svg-LS5aHkO4Y42CoKUj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LS5aHkO4Y42CoKUj .rough-node .label,#mermaid-svg-LS5aHkO4Y42CoKUj .node .label,#mermaid-svg-LS5aHkO4Y42CoKUj .image-shape .label,#mermaid-svg-LS5aHkO4Y42CoKUj .icon-shape .label{text-align:center;}#mermaid-svg-LS5aHkO4Y42CoKUj .node.clickable{cursor:pointer;}#mermaid-svg-LS5aHkO4Y42CoKUj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LS5aHkO4Y42CoKUj .arrowheadPath{fill:#333333;}#mermaid-svg-LS5aHkO4Y42CoKUj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LS5aHkO4Y42CoKUj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LS5aHkO4Y42CoKUj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LS5aHkO4Y42CoKUj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LS5aHkO4Y42CoKUj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LS5aHkO4Y42CoKUj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LS5aHkO4Y42CoKUj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LS5aHkO4Y42CoKUj .cluster text{fill:#333;}#mermaid-svg-LS5aHkO4Y42CoKUj .cluster span{color:#333;}#mermaid-svg-LS5aHkO4Y42CoKUj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LS5aHkO4Y42CoKUj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LS5aHkO4Y42CoKUj rect.text{fill:none;stroke-width:0;}#mermaid-svg-LS5aHkO4Y42CoKUj .icon-shape,#mermaid-svg-LS5aHkO4Y42CoKUj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LS5aHkO4Y42CoKUj .icon-shape p,#mermaid-svg-LS5aHkO4Y42CoKUj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LS5aHkO4Y42CoKUj .icon-shape .label rect,#mermaid-svg-LS5aHkO4Y42CoKUj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LS5aHkO4Y42CoKUj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LS5aHkO4Y42CoKUj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LS5aHkO4Y42CoKUj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} yes
yes
no
no
yes
no
yes
no
createUserRecords
getAliveUsers
getCurrentForegroundUserInfo
当前 display 是否默认屏
前台用户不可切换?
只加入 foreground user
并直接 return
遍历 alive users
managed profile?
getDisplayIdForUser
构建 UserRecord
记录登录状态/座位/安全状态/停止状态
追加 Start Guest Session
前台用户可新增用户?
追加 Add User
返回 UserRecord 列表
核心代码结构:
java
@VisibleForTesting
List<UserRecord> createUserRecords() {
List<UserInfo> userInfos = mUserEventManager.getAliveUsers();
List<UserRecord> userRecords = new ArrayList<>(userInfos.size());
UserInfo foregroundUser = mUserEventManager.getCurrentForegroundUserInfo();
if (mDisplayId == mDisplayTracker.getDefaultDisplayId()) {
if (mUserEventManager.isForegroundUserNotSwitchable(foregroundUser.getUserHandle())) {
userRecords.add(UserRecord.create(
foregroundUser,
foregroundUser.name,
false,
false,
true,
mUserIconProvider.getRoundedUserIcon(foregroundUser, mContext),
new OnClickListenerCreator(),
mLockPatternUtils.isSecure(foregroundUser.id),
true,
mDisplayId,
mCarServiceMediator.getSeatString(mDisplayId),
false));
return userRecords;
}
}
for (int i = 0; i < userInfos.size(); i++) {
UserInfo userInfo = userInfos.get(i);
if (userInfo.isManagedProfile()) {
continue;
}
int loggedInDisplayId = mCarServiceMediator.getDisplayIdForUser(userInfo.id);
UserRecord record = UserRecord.create(
userInfo,
userInfo.name,
false,
false,
userInfo.id == foregroundUser.id,
mUserIconProvider.getRoundedUserIcon(userInfo, mContext),
new OnClickListenerCreator(),
mLockPatternUtils.isSecure(userInfo.id),
loggedInDisplayId != INVALID_DISPLAY,
loggedInDisplayId,
mCarServiceMediator.getSeatString(loggedInDisplayId),
mUserPickerSharedState.isStoppingUser(userInfo.id));
userRecords.add(record);
}
userRecords.add(createStartGuestUserRecord());
if (mUserEventManager.canForegroundUserAddUsers()) {
userRecords.add(createAddUserRecord());
}
return userRecords;
}
8.4 关键状态说明
| 字段 | 含义 | 来源 |
|---|---|---|
mIsForeground |
是否为当前 foreground user | userInfo.id == foregroundUser.id |
mIsSecure |
用户是否设置锁屏安全认证 | LockPatternUtils.isSecure(userId) |
mIsLoggedIn |
用户是否已绑定某个 Display | getDisplayIdForUser(userId) != INVALID_DISPLAY |
mLoggedInDisplay |
已登录用户所在 Display | CarServiceMediator.getDisplayIdForUser(userId) |
mSeatLocationName |
Display 对应座位文案 | CarServiceMediator.getSeatString(displayId) |
mIsStopping |
用户是否正在停止 | UserPickerSharedState.isStoppingUser(userId) |
9. 用户生命周期事件刷新链路
9.1 监听注册
UserEventManager 构造时注册 CarUserManager 用户生命周期监听:
java
@Inject
UserEventManager(Context context,
CarServiceMediator carServiceMediator,
UserPickerSharedState userPickerSharedState) {
mUpdateListeners = new SparseArray<>();
mContext = context.getApplicationContext();
mUserLifecycleReceiver = Executors.newSingleThreadExecutor();
mMainHandler = new Handler(Looper.getMainLooper());
mUserManager = mContext.getSystemService(UserManager.class);
mUserPickerSharedState = userPickerSharedState;
mCarServiceMediator = carServiceMediator;
mCarServiceMediator.registerUserChangeEventsListener(
mUserLifecycleReceiver, mFilter, mUserLifecycleListener);
registerUserInfoChangedReceiver();
}
底层 CarUserManager 通过 Binder 向 CarUserService 注册生命周期回调:
java
mReceiver = new LifecycleResultReceiver();
mService.setLifecycleListenerForApp(packageName, filter, mReceiver);
mListeners.put(listener, Pair.create(filter, executor));
9.2 事件处理
java
private void onUserEvent(CarUserManager.UserLifecycleEvent event) {
int eventType = event.getEventType();
int userId = event.getUserId();
if (eventType == USER_LIFECYCLE_EVENT_TYPE_STOPPING) {
mUserPickerSharedState.addStoppingUserId(userId);
} else if (eventType == USER_LIFECYCLE_EVENT_TYPE_STOPPED) {
if (mUserPickerSharedState.isStoppingUser(userId)) {
mUserPickerSharedState.removeStoppingUserId(userId);
}
} else if (eventType == USER_LIFECYCLE_EVENT_TYPE_INVISIBLE) {
mUserInvisibleWaiter.onUserInvisible(userId);
}
runUpdateUsersOnMainThread(userId, eventType);
}
9.3 UI 刷新完整链路
UserPickerAdapter UserPickerActivity UserPickerController UserEventManager CarUserManager CarUserService UserPickerAdapter UserPickerActivity UserPickerController UserEventManager CarUserManager CarUserService #mermaid-svg-sZiZ1Q9WDBBmHiDx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sZiZ1Q9WDBBmHiDx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .error-icon{fill:#552222;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .marker.cross{stroke:#333333;}#mermaid-svg-sZiZ1Q9WDBBmHiDx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sZiZ1Q9WDBBmHiDx p{margin:0;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sZiZ1Q9WDBBmHiDx text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sZiZ1Q9WDBBmHiDx .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-sZiZ1Q9WDBBmHiDx #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .sequenceNumber{fill:white;}#mermaid-svg-sZiZ1Q9WDBBmHiDx #sequencenumber{fill:#333;}#mermaid-svg-sZiZ1Q9WDBBmHiDx #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .messageText{fill:#333;stroke:none;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .labelText,#mermaid-svg-sZiZ1Q9WDBBmHiDx .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .loopText,#mermaid-svg-sZiZ1Q9WDBBmHiDx .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sZiZ1Q9WDBBmHiDx .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .noteText,#mermaid-svg-sZiZ1Q9WDBBmHiDx .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .actorPopupMenu{position:absolute;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-sZiZ1Q9WDBBmHiDx .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sZiZ1Q9WDBBmHiDx .actor-man circle,#mermaid-svg-sZiZ1Q9WDBBmHiDx line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-sZiZ1Q9WDBBmHiDx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} UserLifecycleEvent(Binder callback)mUserLifecycleListeneronUserEvent(event)更新 stopping / invisible 状态runUpdateUsersOnMainThreadOnUpdateUsersListener(userId, eventType)onUserUpdate / createUserRecordscallbacks.onUpdateUsers(records)updateUsers(records)notifyDataSetChanged()
10. 点击用户卡片后的选择逻辑
10.1 每个 UserRecord 持有自己的点击回调
java
static UserRecord create(UserInfo info,
String name,
boolean isStartGuestSession,
boolean isAddUser,
boolean isForeground,
Drawable icon,
OnClickListenerCreatorBase listenerMaker,
boolean isSecure,
boolean isLoggedIn,
int loggedInDisplay,
String seatLocationName,
boolean isStopping) {
UserRecord userRecord = new UserRecord(info, name, isStartGuestSession, isAddUser,
isForeground, icon, isSecure, isLoggedIn, loggedInDisplay,
seatLocationName, isStopping);
listenerMaker.setUserRecord(userRecord);
userRecord.mOnClickListener = listenerMaker.createOnClickListenerWithUserRecord();
return userRecord;
}
10.2 点击入口
java
OnClickListener getOnClickListener(UserRecord userRecord) {
return holderView -> {
if (!mIsUserPickerClickable) {
return;
}
mIsUserPickerClickable = false;
if (userRecord != null && userRecord.mIsAddUser) {
if (mUserEventManager.isUserLimitReached()) {
mDialogManager.showDialog(DIALOG_TYPE_MAX_USER_COUNT_REACHED);
} else {
mDialogManager.showDialog(
DIALOG_TYPE_CONFIRM_ADD_USER,
() -> startAddNewUser());
}
mIsUserPickerClickable = true;
return;
}
handleUserSelected(userRecord);
};
}
这里使用 mIsUserPickerClickable 做防抖和并发保护,避免用户连续点击触发多次用户切换。
10.3 handleUserSelected() 主流程
#mermaid-svg-AqffW152tHtSVhxS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AqffW152tHtSVhxS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AqffW152tHtSVhxS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AqffW152tHtSVhxS .error-icon{fill:#552222;}#mermaid-svg-AqffW152tHtSVhxS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AqffW152tHtSVhxS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AqffW152tHtSVhxS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AqffW152tHtSVhxS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AqffW152tHtSVhxS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AqffW152tHtSVhxS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AqffW152tHtSVhxS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AqffW152tHtSVhxS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AqffW152tHtSVhxS .marker.cross{stroke:#333333;}#mermaid-svg-AqffW152tHtSVhxS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AqffW152tHtSVhxS p{margin:0;}#mermaid-svg-AqffW152tHtSVhxS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AqffW152tHtSVhxS .cluster-label text{fill:#333;}#mermaid-svg-AqffW152tHtSVhxS .cluster-label span{color:#333;}#mermaid-svg-AqffW152tHtSVhxS .cluster-label span p{background-color:transparent;}#mermaid-svg-AqffW152tHtSVhxS .label text,#mermaid-svg-AqffW152tHtSVhxS span{fill:#333;color:#333;}#mermaid-svg-AqffW152tHtSVhxS .node rect,#mermaid-svg-AqffW152tHtSVhxS .node circle,#mermaid-svg-AqffW152tHtSVhxS .node ellipse,#mermaid-svg-AqffW152tHtSVhxS .node polygon,#mermaid-svg-AqffW152tHtSVhxS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AqffW152tHtSVhxS .rough-node .label text,#mermaid-svg-AqffW152tHtSVhxS .node .label text,#mermaid-svg-AqffW152tHtSVhxS .image-shape .label,#mermaid-svg-AqffW152tHtSVhxS .icon-shape .label{text-anchor:middle;}#mermaid-svg-AqffW152tHtSVhxS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AqffW152tHtSVhxS .rough-node .label,#mermaid-svg-AqffW152tHtSVhxS .node .label,#mermaid-svg-AqffW152tHtSVhxS .image-shape .label,#mermaid-svg-AqffW152tHtSVhxS .icon-shape .label{text-align:center;}#mermaid-svg-AqffW152tHtSVhxS .node.clickable{cursor:pointer;}#mermaid-svg-AqffW152tHtSVhxS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AqffW152tHtSVhxS .arrowheadPath{fill:#333333;}#mermaid-svg-AqffW152tHtSVhxS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AqffW152tHtSVhxS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AqffW152tHtSVhxS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AqffW152tHtSVhxS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AqffW152tHtSVhxS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AqffW152tHtSVhxS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AqffW152tHtSVhxS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AqffW152tHtSVhxS .cluster text{fill:#333;}#mermaid-svg-AqffW152tHtSVhxS .cluster span{color:#333;}#mermaid-svg-AqffW152tHtSVhxS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AqffW152tHtSVhxS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AqffW152tHtSVhxS rect.text{fill:none;stroke-width:0;}#mermaid-svg-AqffW152tHtSVhxS .icon-shape,#mermaid-svg-AqffW152tHtSVhxS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AqffW152tHtSVhxS .icon-shape p,#mermaid-svg-AqffW152tHtSVhxS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AqffW152tHtSVhxS .icon-shape .label rect,#mermaid-svg-AqffW152tHtSVhxS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AqffW152tHtSVhxS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AqffW152tHtSVhxS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AqffW152tHtSVhxS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} no
yes
yes
yes
no
no
yes
no
yes
no
yes
no
yes
no
no
yes
yes
no
yes
no
yes
no
点击 UserRecord
mIsUserPickerClickable?
忽略点击
置为不可点击
Add User?
用户数量达到上限?
显示最大用户数 Dialog
显示确认新增用户 Dialog
恢复可点击
handleUserSelected
Worker 线程执行
获取当前 Display 旧用户 prevUserId
点击的是当前已登录用户?
关闭 Activity
用户已在其他 Display 登录
或正在 stopping
或安全用户不可用?
显示 Snackbar
恢复可点击
是否访客入口?
创建 Guest
创建成功?
关闭切换 Dialog
显示失败 Snackbar
获取 guest userId
使用目标 userId
setUserLoginStarted
是否成功设置登录中状态?
返回,避免多屏冲突
是否需停止旧用户?
stopUserAssignedToDisplay
停止成功?
失败恢复
startUserForDisplay
启动成功?
安全用户或已解锁时关闭 Activity
关闭 Dialog
resetUserLoginStarted
10.4 选择逻辑关键代码
java
void handleUserSelected(UserRecord userRecord) {
if (userRecord == null) {
return;
}
mWorker.execute(() -> {
int userId = userRecord.mInfo != null
? userRecord.mInfo.id
: INVALID_USER_ID;
int prevUserId = mCarServiceMediator.getUserForDisplay(mDisplayId);
if ((userId != INVALID_USER_ID && userId == prevUserId)
|| (userRecord.mIsStartGuestSession && isGuestUser(prevUserId))) {
runOnMainHandler(REQ_FINISH_ACTIVITY);
return;
}
boolean isFgUserStart = prevUserId == ActivityManager.getCurrentUser();
if ((userRecord.mIsLoggedIn && userRecord.mLoggedInDisplay != mDisplayId)
|| mUserPickerSharedState.isStoppingUser(userId)
|| (!Flags.supportsSecurePassengerUsers()
&& userRecord.mIsSecure
&& !isFgUserStart)) {
String message;
if (userRecord.mIsStopping) {
message = mContext.getString(
R.string.wait_for_until_stopped_message,
userRecord.mName);
} else if (!Flags.supportsSecurePassengerUsers()
&& userRecord.mIsSecure
&& !isFgUserStart) {
message = mContext.getString(R.string.unavailable_secure_user_message);
} else {
message = mContext.getString(
R.string.already_logged_in_message,
userRecord.mName,
userRecord.mSeatLocationName);
}
runOnMainHandler(REQ_SHOW_SNACKBAR, message);
mIsUserPickerClickable = true;
return;
}
boolean result = false;
try {
if (userRecord.mIsStartGuestSession) {
runOnMainHandler(REQ_SHOW_SWITCHING_DIALOG);
UserCreationResult creationResult = mUserEventManager.createGuest();
if (creationResult == null || !creationResult.isSuccess()) {
runOnMainHandler(REQ_DISMISS_SWITCHING_DIALOG);
runOnMainHandler(REQ_SHOW_SNACKBAR,
mContext.getString(R.string.guest_creation_failed_message));
return;
}
userId = creationResult.getUser().getIdentifier();
}
if (!mUserPickerSharedState.setUserLoginStarted(mDisplayId, userId)) {
return;
}
if (!isFgUserStart && !stopUserAssignedToDisplay(prevUserId)) {
return;
}
runOnMainHandler(REQ_SHOW_SWITCHING_DIALOG);
result = mUserEventManager.startUserForDisplay(
prevUserId, userId, mDisplayId, isFgUserStart);
} finally {
mIsUserPickerClickable = !result;
if (result) {
if (mLockPatternUtils.isSecure(userId)
|| mUserEventManager.isUserRunningUnlocked(userId)) {
runOnMainHandler(REQ_FINISH_ACTIVITY);
}
} else {
runOnMainHandler(REQ_DISMISS_SWITCHING_DIALOG);
mUserPickerSharedState.resetUserLoginStarted(mDisplayId);
}
}
});
}
11. 登录状态与多屏冲突处理
User Picker 的复杂点主要在多 Display、多 User、Occupant Zone 场景下的并发控制。
11.1 当前 Display 已经登录目标用户
如果点击的用户已经是当前 Display 绑定用户,则不做切换,直接关闭页面:
java
if (userId != INVALID_USER_ID && userId == prevUserId) {
runOnMainHandler(REQ_FINISH_ACTIVITY);
return;
}
11.2 用户已在其他 Display 登录
如果目标用户已经登录到其他 Display,当前 Display 不允许重复登录该用户:
java
if (userRecord.mIsLoggedIn && userRecord.mLoggedInDisplay != mDisplayId) {
runOnMainHandler(REQ_SHOW_SNACKBAR, message);
mIsUserPickerClickable = true;
return;
}
提示文案包含目标用户名称与已登录座位名。
11.3 用户正在停止
用户处于 STOPPING 状态时不允许立即重新登录,需要等待 STOPPED 事件后刷新:
java
if (mUserPickerSharedState.isStoppingUser(userId)) {
runOnMainHandler(REQ_SHOW_SNACKBAR, message);
return;
}
11.4 安全乘客用户限制
当 Flags.supportsSecurePassengerUsers() 不支持时,非 foreground start 场景下的 secure user 不允许在乘客屏启动:
java
if (!Flags.supportsSecurePassengerUsers()
&& userRecord.mIsSecure
&& !isFgUserStart) {
runOnMainHandler(REQ_SHOW_SNACKBAR,
mContext.getString(R.string.unavailable_secure_user_message));
return;
}
12. Logout 逻辑
退出当前 Display 用户时,Controller 会重置登录中状态,停止用户,并主动触发一次 UI 刷新:
java
private void logoutUserInternal(int userId) {
mUserPickerSharedState.resetUserLoginStarted(mDisplayId);
mUserEventManager.stopUserUnchecked(userId, mDisplayId);
mUserEventManager.runUpdateUsersOnMainThread(userId, 0);
mIsUserPickerClickable = true;
}
主动刷新很关键:用户停止过程是异步的,如果完全依赖 CarUserManager 的生命周期回调,UI 可能不能第一时间显示 stopping 或 logout 状态。
13. Power Button:关闭乘客屏逻辑
13.1 Activity 到 Controller
java
View powerBtn = mRootView.findViewById(R.id.power_button_icon_view);
powerBtn.setOnClickListener(v -> mController.screenOffDisplay());
if (mIsDriver) {
powerBtn.setVisibility(View.GONE);
}
13.2 Controller 到 CarServiceMediator
java
void screenOffDisplay() {
mCarServiceMediator.screenOffDisplay(mDisplayId);
}
13.3 CarPowerManager 调用
java
void screenOffDisplay(int displayId) {
if (mCarPowerManager == null) {
Slog.e(TAG, "Cannot get power manager.");
return;
}
mCarPowerManager.setDisplayPowerState(displayId, false);
}
13.4 CarPowerManagementService 校验
java
@Override
public void setDisplayPowerState(int displayId, boolean enable) {
CarServiceUtils.assertPermission(mContext, Car.PERMISSION_CAR_POWER);
boolean isNotSelf = Binder.getCallingUid() != Process.myUid();
CarOccupantZoneService occupantZoneService =
CarLocalServices.getService(CarOccupantZoneService.class);
long token = Binder.clearCallingIdentity();
try {
int driverDisplayId = occupantZoneService.getDisplayIdForDriver(
CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
if (displayId == driverDisplayId && isNotSelf) {
throw new UnsupportedOperationException(
"Driver display control is not supported");
}
} finally {
Binder.restoreCallingIdentity(token);
}
mSystemInterface.setDisplayState(displayId, enable);
}
13.5 DisplayInterface 下游处理
java
@Override
public void setDisplayState(int displayId, boolean on) {
CarPowerManagementService carPowerManagementService;
synchronized (mLock) {
carPowerManagementService = mCarPowerManagementService;
if (on && carPowerManagementService != null
&& !carPowerManagementService.canTurnOnDisplay(displayId)) {
return;
}
mDisplayStateSet.put(displayId, on);
}
if (on) {
mWakeLockInterface.switchToFullWakeLock(displayId);
} else {
mWakeLockInterface.switchToPartialWakeLock(displayId);
PowerManagerHelper.goToSleep(
mContext,
displayId,
SystemClock.uptimeMillis());
}
if (carPowerManagementService != null) {
carPowerManagementService.handleDisplayChanged(displayId, on);
}
}
关闭屏幕完整链路:
PowerManager WakeLockInterface DisplayInterface CarPowerManagementService CarPowerManager CarServiceMediator UserPickerController UserPickerActivity PowerManager WakeLockInterface DisplayInterface CarPowerManagementService CarPowerManager CarServiceMediator UserPickerController UserPickerActivity #mermaid-svg-h6rLpNpxtpRoLnOM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-h6rLpNpxtpRoLnOM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-h6rLpNpxtpRoLnOM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-h6rLpNpxtpRoLnOM .error-icon{fill:#552222;}#mermaid-svg-h6rLpNpxtpRoLnOM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-h6rLpNpxtpRoLnOM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-h6rLpNpxtpRoLnOM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-h6rLpNpxtpRoLnOM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-h6rLpNpxtpRoLnOM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-h6rLpNpxtpRoLnOM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-h6rLpNpxtpRoLnOM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-h6rLpNpxtpRoLnOM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-h6rLpNpxtpRoLnOM .marker.cross{stroke:#333333;}#mermaid-svg-h6rLpNpxtpRoLnOM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-h6rLpNpxtpRoLnOM p{margin:0;}#mermaid-svg-h6rLpNpxtpRoLnOM .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h6rLpNpxtpRoLnOM text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-h6rLpNpxtpRoLnOM .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-h6rLpNpxtpRoLnOM .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-h6rLpNpxtpRoLnOM .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-h6rLpNpxtpRoLnOM .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-h6rLpNpxtpRoLnOM #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-h6rLpNpxtpRoLnOM .sequenceNumber{fill:white;}#mermaid-svg-h6rLpNpxtpRoLnOM #sequencenumber{fill:#333;}#mermaid-svg-h6rLpNpxtpRoLnOM #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-h6rLpNpxtpRoLnOM .messageText{fill:#333;stroke:none;}#mermaid-svg-h6rLpNpxtpRoLnOM .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h6rLpNpxtpRoLnOM .labelText,#mermaid-svg-h6rLpNpxtpRoLnOM .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-h6rLpNpxtpRoLnOM .loopText,#mermaid-svg-h6rLpNpxtpRoLnOM .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-h6rLpNpxtpRoLnOM .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-h6rLpNpxtpRoLnOM .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-h6rLpNpxtpRoLnOM .noteText,#mermaid-svg-h6rLpNpxtpRoLnOM .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-h6rLpNpxtpRoLnOM .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h6rLpNpxtpRoLnOM .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h6rLpNpxtpRoLnOM .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h6rLpNpxtpRoLnOM .actorPopupMenu{position:absolute;}#mermaid-svg-h6rLpNpxtpRoLnOM .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-h6rLpNpxtpRoLnOM .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h6rLpNpxtpRoLnOM .actor-man circle,#mermaid-svg-h6rLpNpxtpRoLnOM line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-h6rLpNpxtpRoLnOM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} screenOffDisplay()screenOffDisplay(displayId)setDisplayPowerState(displayId, false)binder call权限校验 + Driver Display 校验setDisplayState(displayId, false)mDisplayStateSet.put(displayId, false)switchToPartialWakeLock(displayId)goToSleep(displayId, uptime)handleDisplayChanged(displayId, false)
WakeLockInterface 中,关屏会切换到 Partial WakeLock:
java
@Override
public void switchToPartialWakeLock(int displayId) {
Pair<WakeLock, WakeLock> wakeLockPair;
synchronized (mLock) {
wakeLockPair = mPerDisplayWakeLocks.get(displayId);
}
if (wakeLockPair == null) {
Slogf.w(TAG, "WakeLocks for display %d is null", displayId);
return;
}
WakeLock partialWakeLock = wakeLockPair.second;
WakeLock fullWakeLock = wakeLockPair.first;
if (!partialWakeLock.isHeld()) {
partialWakeLock.acquire();
}
if (fullWakeLock.isHeld()) {
fullWakeLock.release();
}
}
PARTIAL_WAKE_LOCK 保证 CPU 继续运行,但允许屏幕和键盘背光关闭。
14. Header 状态与 UI 行为
HeaderState 由 Controller 初始化并在用户状态变化时更新。主要区分两类:
| 状态 | 触发条件 | UI 行为 |
|---|---|---|
HEADER_STATE_LOGOUT |
当前 Display 没有绑定 User | 展示登录选择态,通常不显示 Logout/Back。 |
HEADER_STATE_CHANGE_USER |
当前 Display 已绑定 User | 展示切换用户态,显示 Logout/Back。 |
建议检查 setupHeaderBar(HeaderState headerState) 中的具体实现,确认以下 UI 元素是否与产品需求一致:
- Header 文案。
- Logout 按钮显隐。
- Back 按钮显隐。
- 不同 Display/Seat 下文案是否准确。
15. 建议补充关注点
15.1 多屏座位映射
mCarServiceMediator.getUserForDisplay(displayId) 与 getDisplayIdForUser(userId) 是理解 User Picker 的关键。它们背后依赖 Occupant Zone:
text
OccupantZone -> DisplayId -> UserId
UserId -> DisplayId(DISPLAY_TYPE_MAIN)
DisplayId -> SeatLocationName
建议在调试时重点 dump:
bash
adb shell dumpsys car_service --services CarOccupantZoneService
adb shell dumpsys car_service --services CarUserService
adb shell dumpsys activity users
15.2 并发登录保护
UserPickerSharedState.setUserLoginStarted(displayId, userId) 用于避免多个屏幕几乎同时选择同一个用户。建议在问题定位时增加以下日志:
java
Slog.d(TAG, "setUserLoginStarted: displayId=" + displayId + ", userId=" + userId);
需要关注的异常路径:
- A 屏点击 user 10,B 屏同时点击 user 10。
- 用户还处于 STOPPING,另一个屏幕立即尝试启动。
- Guest 创建成功,但 startUserForDisplay 失败。
- stopUserAssignedToDisplay 成功但新用户启动失败。
15.3 Secure User 场景
当前逻辑中 secure user 受 Flags.supportsSecurePassengerUsers() 影响。需要结合产品策略确认:
- 是否允许乘客屏登录设置了锁屏密码的用户。
- 如果允许,解锁界面由哪个 Display 承载。
- 如果不允许,文案是否足够明确。
15.4 Adapter 更新方式
当前使用:
java
mAdapter.updateUsers(users);
mAdapter.notifyDataSetChanged();
如果用户列表数量较多或动画要求较高,可以考虑使用 DiffUtil 优化刷新,但要注意 UserRecord 中 Drawable、OnClickListener、运行态字段的相等性定义,否则容易引入 UI 状态不一致。
参考实现:
java
final class UserRecordDiffCallback extends DiffUtil.Callback {
private final List<UserRecord> mOldList;
private final List<UserRecord> mNewList;
UserRecordDiffCallback(List<UserRecord> oldList, List<UserRecord> newList) {
mOldList = oldList;
mNewList = newList;
}
@Override
public int getOldListSize() {
return mOldList.size();
}
@Override
public int getNewListSize() {
return mNewList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
UserRecord oldRecord = mOldList.get(oldItemPosition);
UserRecord newRecord = mNewList.get(newItemPosition);
if (oldRecord.mIsAddUser || newRecord.mIsAddUser) {
return oldRecord.mIsAddUser == newRecord.mIsAddUser;
}
if (oldRecord.mIsStartGuestSession || newRecord.mIsStartGuestSession) {
return oldRecord.mIsStartGuestSession == newRecord.mIsStartGuestSession;
}
if (oldRecord.mInfo == null || newRecord.mInfo == null) {
return false;
}
return oldRecord.mInfo.id == newRecord.mInfo.id;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
UserRecord oldRecord = mOldList.get(oldItemPosition);
UserRecord newRecord = mNewList.get(newItemPosition);
return oldRecord.mIsForeground == newRecord.mIsForeground
&& oldRecord.mIsSecure == newRecord.mIsSecure
&& oldRecord.mIsLoggedIn == newRecord.mIsLoggedIn
&& oldRecord.mLoggedInDisplay == newRecord.mLoggedInDisplay
&& oldRecord.mIsStopping == newRecord.mIsStopping
&& Objects.equals(oldRecord.mName, newRecord.mName)
&& Objects.equals(oldRecord.mSeatLocationName, newRecord.mSeatLocationName);
}
}
使用方式:
java
void updateUsers(List<UserRecord> users) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
new UserRecordDiffCallback(mUsers, users));
mUsers.clear();
mUsers.addAll(users);
diffResult.dispatchUpdatesTo(this);
}
注意:如果改为 DiffUtil,Activity 中不应再调用 notifyDataSetChanged(),否则 Diff 的局部刷新效果会被覆盖。
16. 调试建议
16.1 查看当前用户与运行态
bash
adb shell dumpsys activity users
adb shell am get-current-user
16.2 查看 Car User / Occupant Zone
bash
adb shell dumpsys car_service --services CarUserService
adb shell dumpsys car_service --services CarOccupantZoneService
16.3 查看 SystemUI Activity 与 Display
bash
adb shell dumpsys activity activities | grep -i UserPickerActivity -A 30
adb shell dumpsys display
16.4 建议日志点
java
Slog.d(TAG, "createUserRecords: displayId=" + mDisplayId);
Slog.d(TAG, "handleUserSelected: targetUserId=" + userId
+ ", prevUserId=" + prevUserId
+ ", displayId=" + mDisplayId
+ ", isFgUserStart=" + isFgUserStart);
Slog.d(TAG, "startUserForDisplay result=" + result);
17. 总结
UserPickerActivity 本身并不直接执行用户切换,它主要完成:
- 以 System User 身份启动自身。
- 初始化全屏 User Picker 布局。
- 初始化按钮、RecyclerView、底部栏、Dialog/Snackbar 管理器。
- 通过
UserPickerController获取用户列表并响应用户点击。 - 通过回调刷新 Adapter、Header 和关闭页面。
核心业务逻辑集中在 UserPickerController:
- 通过
UserEventManager.getAliveUsers()获取可展示用户。 - 通过
CarServiceMediator判断 Display/User/Seat 关系。 - 通过
UserPickerSharedState处理 stopping 与 login-started 并发状态。 - 通过
UserEventManager.startUserForDisplay()完成新用户在目标 Display 上启动。 - 通过
CarPowerManager.setDisplayPowerState()完成非 Driver Display 关闭屏幕。
整个流程可以概括为:
text
Activity 初始化 UI
-> Controller 注册回调与用户事件监听
-> UserEventManager 从 UserManager / CarUserManager 获取用户与生命周期事件
-> Controller 构建 UserRecord 列表
-> Adapter 展示用户状态
-> 点击用户后 Controller 处理冲突校验、访客创建、旧用户停止、新用户启动
-> 生命周期事件回流刷新 UI
18. 代码校验记录
- 文档中的新增 Java 代码片段已按 Java 语法结构检查,未发现括号、字符串、泛型尖括号不匹配问题。
- 涉及 Android Framework / Car API 的代码片段依赖 AOSP 编译环境,建议在
android-15.0.0_r17源码树中通过对应模块编译做最终校验。 DiffUtil参考实现使用标准 AndroidX API;如果 Car SystemUI 当前模块未引入 AndroidX RecyclerView DiffUtil 依赖,需要先确认 Soong/Gradle 依赖,否则保持现有notifyDataSetChanged()实现即可。
建议最终验证命令:
bash
source build/envsetup.sh
lunch <target>
m packages/apps/Car/SystemUI
---------------------------------------------------End Line-----------------------------------------------------