36 · 可见即可说语音链路

蓝牙页”可见即可说”:语音激活 → 列表亮序号角标 → 用户喊”连接第 3 个”/“连接小米手环” → 执行的端到端剖析。 行号基线:fuben_micarsettings dev HEAD 2994ee8ba(2026-09-10 fetch 实证)。 核心文件:view/BluetoothMainEntrySettingsFragment.javacontroller/Bluetooth{Bonded,Unbonded}DevicesPrefController.javapreference/NewBluetoothDevicePreference.javabase/settingsBaseLib/.../voiceassist/ViewDescriptionCompat.kt;框架侧 mi.car.core.viewpage.*(平台 javalib jar 反编译实证)。

0. 一句话结论

语音不直接调蓝牙 API。 框架做的是「语音匹配语义标签 → 定位 View → 合成一次点击」,执行落回手指点击的同一条 OnPreferenceClickListener 路径。Settings 侧的全部职责 = 给每个条目 View 贴 MiCarViewDescription 标签(热词/序号/意图词)+ 维护序号角标。

1. 三方角色与分层

角色位置职责
App(车设)本仓源码贴标签、亮角标、提供正常点击回调
平台框架mi.car.core.viewpage(framework 内置,jar 反编译可见)遍历 View 树收集标签成 MiCarViewPageInfo;维护 per-window 回调通道;路由执行动作
语音服务(小爱)系统服务(本仓无源码)拉标签做 ASR/NLU 匹配;下发 performViewPageAction;播报结果

2. 进入/退出语音模式:notifyVoiceActive 链路

语音服务 setEnabled(true/false)            ← IMiCarViewPageManagerClient.setEnabled(boolean,boolean)(AIDL)
  → 平台框架回调 View 上的 MiCarViewListener
  → BluetoothMainEntrySettingsFragment.onViewCreated 里挂的监听 (:110-118)
      MiCarView.setViewListener(getListView(), listener)
  → setViewPageEnabled(viewPageEnabled)
  → notifyVoiceActive(viewPageEnabled)     (:126-145)

页面销毁摘监听onDestroyView():121-124 setViewListener(getListView(), null)——防框架异步回调打到死 Fragment。

notifyVoiceActive 本体三段:

  1. 守卫(:128-131):getContext()==null || !isAdded() 直接 abort——回调是框架异步打来的,与 onDestroyView 摘监听存在竞态,不挡则下面 use() 拿 PreferenceManager 会崩。
  2. 取两个 controller(:132-141):use(Class, keyResId)BaseXmlParserSettingsFragment.java:242)按 preference key 取已实例化 controller;判空(availability 过滤后可能不存在)。
  3. 分发bondedController.updateVoiceAssistActive(flag) + unBondedController.updateVoiceAssistActive(flag)

3. Controller 侧:序号编排(连续编号的关键)

BondedBluetoothBondedDevicesPrefController.java):

  • updateVoiceAssistActive(:107-111):存 mIsVoiceAssistActiverefreshVoiceAssistViewOrder
  • refreshVoiceAssistViewOrder(:117-140):遍历分组、跳过不可见项,逐设备 setIsVoiceAssistActive + setVoiceAssistDeviceOrder(visiblePrefIndex)——序号从 0 起

UnbondedBluetoothUnbondedDevicesPrefController.java):

  • updateVoiceAssistActive(:568-572)同款。
  • refreshVoiceAssistViewOrder(:574-598):先读已配对分组的条目数写入 mDeviceIndexOffset(:576-580),可用设备序号 = 可见序号 + offset——未配对接着已配对连续编号(已配对 3 台 → 附近设备从 4 开始)。
  • mDeviceIndexOffsetAtomicInteger(:71):回调线程/后台线程都可能读写。

快照刷新点(状态变化后不重刷则标签陈旧):

  • bonded::397 / :415(设备状态变化后补刷);新建 preference 时 :149 补 setIsVoiceAssistActive
  • unbonded:addPreference :494 对新条目补 flag。

4. Preference 侧:标签内容与角标(NewBluetoothDevicePreference)

setVoiceAssistDeviceOrder(index)(:252-263) 构造语音语义:

mViewDescription = new MiCarViewDescription();
mViewDescription.setTitle(设备名);
mViewDescription.getHotwords().add(设备名);            // 热词 = 设备名 → 可直呼其名
mViewDescription.setIndex(index);                       // 序号 → "第 N 个"
mViewDescription.setTextIntent(isConnected() ? "断开" : "连接");  // 意图词,快照取值
notifyDataChanged();                                    // 触发 rebind

rebind 时(onBindViewHolder :211-216)

  • ViewDescriptionCompat.setViewDescription(holder.itemView, mViewDescription)——把语义挂到 item View 供框架读取;
  • updateViewIndexMarker(:222-243):mIsVoiceAssistActive && mViewDescription != null 时显示左侧序号角标 item_index_for_voice显示值 = index+1(:233,用户念的从 1 开始);否则隐藏。角标自身 setDescriptionHide(:242)防被语音误识别成可点元素。

ViewDescriptionCompatbase/settingsBaseLib/.../voiceassist/ViewDescriptionCompat.kt):

  • isSupportVoiceAssist()(:25-38):反射探测 mi.car.core.viewpage.MiCarView#setViewDescription 是否存在,不支持的平台所有调用打日志直通——老平台兼容设计。
  • setDescriptionHide(vararg views)(:40-54):给 View 贴 visibility=GONE 的空描述,把装饰性元素(summary/角标)从语音树里摘出去。
  • VoiceAssistConstant.DEBUG 打开后有逐条 setViewDescription 日志(:78-80)。

5. 平台框架机制(反编译实证,xcd_javalib_1129.jar

MiCarView 是纯 facade

// 反编译 MiCarView.setViewDescription:
view.getMiCarStub().setMiCarDescription(desc)   // 每个 View 挂了 IMiCarViewStub(小米改的 framework View)

IMiCarViewStub 方法面(摘):setMiCarDescription/getMiCarDescriptiononViewClicked()notifyViewShowIndex()setViewListener(MiCarViewListener)getViewPageEnabledStatus()updateText/updateCheckStatus/updateCurrentProgress(开关/滑杆状态同步)、onDetachedFromWindowInternal()

MiCarViewDescription = Parcelable 语义包

字段(反编译实证):title/subTitlehotwords[]indextextIntent/textIntents[]matchType(FUZZY/COMPLETE/COMPLEX/NOT_MATCH/DEFAULT)、componentType(BUTTON/TEXT_LIST/ORDINAL/SWITCH/SLIDER/TAB…)、priorityisChecked/isSelectedpageTypecategoryActionAdjustMethod 等。

AIDL 五接口(语音服务 ↔ 管理器 ↔ App 窗口)

语音服务(小爱)                    IMiCarViewPageManager(系统侧)                App 进程(framework View 层)
    │  registerViewPageInfosUpdateListener / getViewPageInfos()  │  updatePageInfo(MiCarViewPageInfo)
    │ ────────────────────────► 管理器 ◄────────────────────────  │  (View 树遍历收集各 View 标签)
    │  addClient → IMiCarViewPageManagerClient.setEnabled() ───►  │  → MiCarViewListener.setViewPageEnabled
    │                                                            │    (=notifyVoiceActive 触发源)
    │  performViewPageAction(actionId, targetId, index, Bundle)  │
    │ ────────────────────────► 按 IWindow 路由 ───────────────► │ IMiCarViewPageInteractionConnection
    │                                                            │   .performViewPageAction(int,int,Bundle)
    │  onActionPerformResult / onActionPerformFailedReason ◄────  │  → 定位 View → 合成点击
    │  (TTS 播报"好的/没找到")                                    │  → onActionPerformResult 回传

窗口级通道:addViewPageInteractionConnection(IWindow, IMiCarViewPageInteractionConnection, pkg, type) / removeViewPageInteractionConnection(IWindow)——执行动作按窗口精确路由到 App 进程。

6. 用户喊话的匹配与执行

用户说匹配字段命中
”连接小米手环”hotwords(=设备名),受 matchType 控制模糊/全匹配该设备条目
”连接第 3 个”index(componentType=ORDINAL,屏幕角标 index+1 就是可念的数字)序号 3 的条目
”连接” / “断开”textIntent动作语义

执行链:语音服务 → performViewPageAction → 路由到前台窗口 → App 进程 framework 层对目标 View 合成点击onViewClicked 标记)→ 落到该条目正常点击监听。

未配对设备条目即 BluetoothUnbondedDevicesPrefController.addPreferencesetOnPreferenceClickListener(:496-545):mIsHandlePair 防抖 → 后台线程 stopDiscovery() + 注册配对回调 → MiBluetoothUtils.pair(...)——与手指点击逐行一致

7. 排障推论(语音类问题的两条铁律)

  1. 语音能做的事 ≤ 手指能做的事。条目 setEnabled(false)、防抖拦截、守卫拦截,对语音指令同样生效。遇”语音说连接没反应”,先用手指点同一条目复现——手点失败则与语音链路无关。
  2. 标签是贴标签时刻的快照textIntent 按当时 isConnected() 取一次(:259-261),设备连接状态变化后若无人调 refreshVoiceAssistViewOrder,语音意图即陈旧(“连接”被理解成”断开”类单,先查 §3 刷新点覆盖)。同理设备名改名后 hotwords 也是旧值。

8. 与既有篇目关系

  • 01 §4:扫描列表角标排序的一处提及(本篇展开为全链路)。
  • 73 设置侧:grep 澄清里的 isVoiceAssistActive 即本篇主题。
  • 已配对/未配对列表自身的增删与状态机见 01 / 34