Files
Obsidian/项目文档/同力/蓝牙音乐/蓝牙音乐模块功能流程梳理_2026-07-13.md
2026-11-05 14:35:04 +08:00

23 KiB
Raw Permalink Blame History

蓝牙音乐模块功能流程梳理

更新时间:2026-07-13
工程路径:F:\zdCode\tongli
当前蓝牙音乐提交:8ec79048 modify(YX_BlueTooth):新增蓝牙音乐功能

1. 一句话结论

蓝牙音乐模块不是本地音乐播放器,而是 车机作为 A2DP Sink 接收手机音频 + 作为 AVRCP Controller 控制手机播放器。

也就是说:

  • 音频从手机通过蓝牙 A2DP 传到车机。
  • 播放、暂停、上一首、下一首通过 AVRCP 控制手机端播放器。
  • 歌名、歌手、播放进度、总时长来自手机蓝牙媒体会话回调。
  • 当前 UI 按老项目效果固定显示默认蓝牙音乐封面,不加载手机专辑图。

2. 模块边界

2.1 负责的功能

功能 当前实现
蓝牙音乐入口 主界面左侧“蓝牙音乐”菜单进入 BtMusicFragment
独立音乐页 BluetoothMusicActivity 已注册,但当前主要入口是主界面 Fragment
A2DP 状态 判断 A2DP 是否连接,未连接时显示连接提示
A2DP 自动连接 已有手机设备且 A2DP 未连接时,尝试连接 A2DP
播放控制 播放、暂停、上一首、下一首
歌曲信息 歌名、歌手、当前时间、总时长、进度
音频焦点 播放前申请蓝牙音乐焦点,焦点丢失时暂停/静音
UI 对齐 已按老项目蓝牙音乐页 UI 对齐

2.2 不负责的功能

不负责项 说明
本地音乐扫描 不扫描 U 盘、本地文件或歌单
本地解码播放 不直接解码 mp3/flac 等媒体文件
手机歌单浏览 当前只做基础播放控制,不展示手机媒体库列表
专辑封面展示 当前按老项目固定显示默认蓝牙音乐图
蓝牙配对搜索完整流程 属于蓝牙设置模块

3. 关键代码入口

层级 文件 作用
主入口 YX_BlueTooth/src/main/java/com/yaxon/bluetooth/main/MainActivity.java 左侧菜单切换到蓝牙音乐页
Fragment UI YX_BlueTooth/src/main/java/com/yaxon/bluetooth/fragment/BtMusicFragment.java 主界面内的蓝牙音乐页面
独立 Activity YX_BlueTooth/src/main/java/com/yaxon/bluetooth/main/BluetoothMusicActivity.java 独立蓝牙音乐页面
ViewModel YX_BlueTooth/src/main/java/com/yaxon/bluetooth/viewmodel/BtMusicViewModel.java UI 与 SDK 之间的桥接层
控制门面 YX_BluetoothSDK/src/main/java/com/yaxon/bluetoothsdk/manager/BtMusicControlManager.java 对外提供蓝牙音乐控制 API
音乐管理 YX_BluetoothSDK/src/main/java/com/yaxon/bluetoothsdk/modules/BtMusicManager.java 处理 AVRCP、A2DP、回调分发
AVRCP 客户端 YX_BluetoothSDK/src/main/java/com/yaxon/bluetoothsdk/platform/BluetoothAvrcpClient.java 连接系统蓝牙媒体服务,发送播放控制
MediaBrowser YX_BluetoothSDK/src/main/java/com/yaxon/bluetoothsdk/platform/MediaBrowserClient.java 连接 com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService
音频焦点 YX_BluetoothSDK/src/main/java/com/yaxon/bluetoothsdk/audio/BtMusicAudioManager.java 申请/释放音频焦点,处理焦点变化
A2DP 设置 YX_BluetoothSDK/src/main/java/com/yaxon/bluetoothsdk/manager/BtSettingControlManager.java 判断/连接 A2DP

4. 总体架构流程

这一节先看分层,再看两条主链路:控制链路 和 回调链路。这样比一张横向大图更清楚。

4.1 分层架构图

图里只保留层级关系,详细类名放到下方表格,避免图过大。

flowchart TB
    A["页面层"]
    B["ViewModel"]
    C["SDK 管理层"]
    D["系统蓝牙服务"]
    E["手机端"]

    A --> B
    B --> C
    C --> D
    D --> E
层级 关键类 作用
UI BtMusicFragment / BluetoothMusicActivity 显示界面,接收按钮点击
ViewModel BtMusicViewModel 连接 UI 和 SDK,分发 LiveData
SDK 控制 BtMusicControlManager / BtSettingControlManager 对外提供播放控制、A2DP 连接接口
业务管理 BtMusicManager 组织 AVRCP、A2DP、焦点和回调
AVRCP 控制 BluetoothAvrcpClient / MediaBrowserClient 控制手机播放器,接收媒体状态
A2DP 音频 BluetoothA2dpSinkClient 接收手机蓝牙音频
音频焦点 BtMusicAudioManager / AudioManager 申请焦点、静音/解除静音、处理焦点丢失
系统蓝牙 BluetoothMediaBrowserService / A2DP Sink Service Android 蓝牙系统服务
手机 手机音乐播放器 / 手机蓝牙音频 真正播放音乐并输出 A2DP 音频

SDK 管理层内部再分三条线:

内部链路 关键类 解决的问题
AVRCP 控制 BtMusicManager / BluetoothAvrcpClient / MediaBrowserClient 播放、暂停、上一首、下一首
A2DP 音频 BtSettingControlManager / BluetoothA2dpSinkClient 建立手机到车机的音频通道
音频焦点 BtMusicAudioManager / AudioManager 决定蓝牙音乐能不能出声、是否要暂停或静音

4.2 播放控制链路

用户点按钮时,命令从上往下走,最后控制手机播放器。

flowchart TB
    A["用户操作"]
    B["页面处理"]
    C["ViewModel 转发"]
    D["SDK 控制"]
    E["系统媒体控制"]
    F["手机播放器"]

    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
步骤 代码定位 说明
用户操作 iv_bt_music_state / iv_bt_music_pre / iv_bt_music_next 用户点击播放控制按钮
页面处理 BtMusicFragment.onClick / BluetoothMusicActivity.onClick 判断操作类型
ViewModel 转发 BtMusicViewModel.playBtMusic / pauseBtMusic / preBtMusic / nextBtMusic UI 不直接碰 SDK
SDK 控制 BtMusicControlManager / BtMusicManager 申请焦点并组织 AVRCP 命令
系统媒体控制 BluetoothAvrcpClient / MediaBrowserClient / TransportControls 调用系统蓝牙媒体控制
手机播放器 手机音乐 App 最终执行播放、暂停、切歌

4.3 状态回调链路

手机端播放状态变化后,状态从下往上回到 UI。

flowchart TB
    A["手机播放器"]
    B["系统媒体服务"]
    C["SDK 回调处理"]
    D["ViewModel LiveData"]
    E["页面刷新"]

    A --> B
    B --> C
    C --> D
    D --> E
步骤 代码定位 说明
手机播放器 手机音乐 App 歌曲、进度、播放状态变化
系统媒体服务 BluetoothMediaBrowserService Android 蓝牙 AVRCP Controller 媒体服务
SDK 回调处理 MediaBrowserClient / BluetoothAvrcpClient / BtMusicManager 接收并转换媒体状态
ViewModel LiveData BtMusicViewModel.mLiveDataMusicInfo / mLiveDataPlayState 把状态推给 UI
页面刷新 BtMusicFragment / BluetoothMusicActivity 刷新按钮、歌名、时间、进度

4.4 A2DP 与音频焦点链路

这条链路决定车机是否能真正出声。

先看 A2DP 音频通道:

flowchart TB
    A["进入音乐页"]
    B["检查 A2DP"]
    C["连接手机 A2DP"]
    D["音频通道就绪"]
    E["车机扬声器输出"]

    A --> B
    B --> C
    C --> D
    D --> E

再看音频焦点:

flowchart TB
    A["点击播放"]
    B["申请音频焦点"]
    C["解除 A2DP 静音"]
    D["发送播放命令"]
    E["手机开始播放"]

    A --> B
    B --> C
    C --> D
    D --> E
链路 成功条件 失败表现
A2DP 音频通道 手机 A2DP 已连接或连接成功 UI 保持未连接,车机无蓝牙音乐声
音频焦点 requestAudioFocus 成功 手机可能在播,但车机可能无声或被其他音源压住
AVRCP 控制 TransportControls 可用 播放/暂停/切歌命令可能无效

核心理解:

  • UI 不直接操作系统蓝牙。
  • UI 只调用 BtMusicViewModel。
  • BtMusicViewModel 调 BtMusicControlManager 和 BtSettingControlManager。
  • 真正播放控制走 BluetoothAvrcpClient -> MediaBrowserClient -> TransportControls。
  • 真正音频通道走 A2DP Sink。
  • 真正能不能出声,还取决于 A2DP 是否连接和音频焦点是否申请成功。

5. 初始化流程

蓝牙 SDK 初始化在应用启动时完成。

flowchart TD
    A["应用启动"]
    B["初始化蓝牙 SDK"]
    C["创建音乐管理器"]
    D["准备控制与音频能力"]
    E["注册媒体回调"]

    A --> B
    B --> C
    C --> D
    D --> E

涉及文件:

  • BluetoothApplication.java
  • BluetoothSdkApplication.java
  • BtMusicControlManager.java
  • BtMusicManager.java
  • BluetoothAvrcpClient.java
  • MediaBrowserClient.java
  • BtMusicAudioManager.java

初始化细节:

步骤 代码定位 结果
应用启动 BluetoothApplication 进入蓝牙 SDK 应用基类
初始化蓝牙 SDK BluetoothSdkApplication.setPlatform 初始化各蓝牙业务模块
创建音乐管理器 BtMusicControlManager.init 持有 BtMusicManager
准备控制与音频能力 BluetoothAvrcpClient / BtMusicAudioManager / BluetoothA2dpSinkClient 准备 AVRCP、焦点、A2DP
注册媒体回调 MediaBrowserClient 连接系统媒体服务并监听状态

6. 入口切换流程

主界面左侧菜单有四个入口:

位置 入口
0 蓝牙拨号
1 联系人
2 蓝牙音乐
3 蓝牙设置

Constant.TAB_BLUETOOTH_MUSIC = 2。

flowchart TD
    A["点击蓝牙音乐"]
    B["切换菜单位置"]
    C["更新左侧选中态"]
    D["切到音乐 Fragment"]
    E["显示音乐页面"]

    A --> B
    B --> C
    C --> D
    D --> E

关键代码:

  • MainActivity.setPositionFragment
  • Constant.TAB_BLUETOOTH_MUSIC
  • BtMusicFragment

入口代码定位:

步骤 代码定位 说明
点击蓝牙音乐 MainActivity.onItemClick 左侧菜单点击
切换菜单位置 setPositionFragment(position = 2) 2 对应蓝牙音乐
更新左侧选中态 mMenuAdapter.setSelectedPosition 菜单高亮
切到音乐 Fragment toFragment(mBtMusicFragment) 页面容器替换
显示音乐页面 fragment_bt_music.xml 展示蓝牙音乐 UI

7. 页面 UI 状态流程

蓝牙音乐 UI 由 isBtConnect 控制两种状态。

flowchart TD
    A["进入音乐页"]
    B{"A2DP 已连接"}
    C["音乐控制面板"]
    D["未连接提示"]

    A --> B
    B -->|是| C
    B -->|否| D

UI 文件:

  • fragment_bt_music.xml
  • activity_bluetooth_music.xml

主要控件:

控件 ID 作用
iv_bt_music_album 默认蓝牙音乐封面
tv_bt_music_title 歌曲名
tv_bt_music_artist 歌手
seek_bt_music 播放进度展示,不允许拖动
tv_cur_time 当前播放时间
tv_song_time 总时长
iv_bt_music_pre 上一首
iv_bt_music_state 播放 / 暂停
iv_bt_music_next 下一首
ll_bt_connect_panel 未连接提示页
tv_connect_btn 连接蓝牙按钮,目前未实现跳转

状态对应关系:

状态 显示内容 触发条件
音乐控制面板 默认封面、歌名、歌手、进度条、三个控制按钮 isA2dpConnected == true
未连接提示 连接手机提示、连接蓝牙按钮 isA2dpConnected == false

8. A2DP 连接流程

A2DP 是手机音乐音频传到车机的通道。

进入页面后的检查:

flowchart TD
    A["进入音乐页"]
    B["检查 A2DP"]
    C["获取手机设备"]
    D["发起 A2DP 连接"]
    E["等待连接结果"]

    A --> B
    B --> C
    C --> D
    D --> E

分支规则:

判断点 是 否
A2DP 是否已连接 显示音乐控制面板 继续获取已连接手机设备
已连接设备是否为手机 检查 A2DP 是否正在连接 保持未连接状态
A2DP 是否正在连接 等待连接结果 调用 connectA2dp(mac)

调用定位:

步骤 代码定位
进入音乐页 BtMusicFragment.onResume / BluetoothMusicActivity.onResume
检查 A2DP BtMusicViewModel.isA2dpConnected
获取手机设备 BtMusicFragment.btMusicInfo / BluetoothMusicActivity.btMusicInfo
发起连接 BtMusicViewModel.connectA2dp
底层连接 BtSettingControlManager.connectA2dp / BluetoothA2dpSinkClient

触发位置:

  • BtMusicFragment.btMusicInfo
  • BluetoothMusicActivity.btMusicInfo
  • BtMusicViewModel.connectA2dp
  • BtSettingControlManager.connectA2dp

连接结果回调:

flowchart TD
    A["A2DP 状态变化"]
    B["SDK 连接监听"]
    C["音乐管理器处理"]
    D["LiveData 通知"]
    E["UI 切换状态"]

    A --> B
    B --> C
    C --> D
    D --> E
步骤 代码定位 说明
A2DP 状态变化 系统蓝牙服务 A2DP 连接或断开
SDK 连接监听 BluetoothConnectListenerClient 接收系统连接状态
音乐管理器处理 BtMusicManager.onA2dpConnect / onA2dpDisconnect 清理或恢复蓝牙音乐状态
LiveData 通知 BtMusicViewModel.mLiveDataA2dpConnectStatus 通知 UI
UI 切换状态 BtMusicFragment / BluetoothMusicActivity 显示音乐页或未连接页

9. 播放控制流程

9.1 点击播放 / 暂停

flowchart TD
    A["点击按钮"]
    B{"当前在播放"}
    C["走暂停链路"]
    D["走播放链路"]
    E["手机播放器响应"]

    A --> B
    B -->|是| C
    B -->|否| D
    C --> E
    D --> E
分支 调用顺序 说明
播放 onClick -> isMusicPlaying == false -> playBtMusic -> requestFocus -> AVRCP play 先拿焦点,再发送播放
暂停 onClick -> isMusicPlaying == true -> pauseBtMusic -> AVRCP pause 暂停不需要重新申请焦点
UI 更新 mLiveDataPlayState -> updatePlayState 最终以回调状态刷新图标

9.2 点击上一首 / 下一首

flowchart TD
    A["点击切歌"]
    B["ViewModel 转发"]
    C["SDK 申请焦点"]
    D["发送 AVRCP 命令"]
    E["手机播放器切歌"]

    A --> B
    B --> C
    C --> D
    D --> E

底层实际命令:

操作 页面入口 ViewModel 最终调用
播放 onClick playBtMusic TransportControls.play()
暂停 onClick pauseBtMusic TransportControls.pause()
下一首 onClick nextBtMusic TransportControls.skipToNext()
上一首 onClick preBtMusic TransportControls.skipToPrevious()

10. 歌曲信息与播放状态刷新流程

手机通过系统蓝牙 MediaBrowser 回传媒体状态。

播放状态回调:

flowchart TD
    A["手机播放状态变化"]
    B["系统媒体回调"]
    C["SDK 转换状态"]
    D["LiveData 通知"]
    E["刷新按钮和进度"]

    A --> B
    B --> C
    C --> D
    D --> E

歌曲信息回调:

flowchart TD
    A["手机歌曲变化"]
    B["系统元数据回调"]
    C["SDK 转换歌曲信息"]
    D["LiveData 通知"]
    E["刷新歌名和时长"]

    A --> B
    B --> C
    C --> D
    D --> E

代码定位:

回调类型 SDK 入口 ViewModel 入口 UI 刷新内容
播放状态 onPlaybackStateChanged onPlayMusicState / onPlayMusicInfo 播放按钮、当前进度、当前时间
歌曲信息 onMetadataChanged onPlayMusicInfo 歌名、歌手、总时长

字段来源:

字段 来源
歌名 MediaMetadata.METADATA_KEY_TITLE
歌手 MediaMetadata.METADATA_KEY_ARTIST
专辑 MediaMetadata.METADATA_KEY_ALBUM
总时长 MediaMetadata.METADATA_KEY_DURATION
当前进度 PlaybackStateCompat.getPosition()
播放状态 PlaybackStateCompat.STATE_PLAYING

当前 UI 特殊点:

  • albumUrl 虽然会接收,但 UI 不展示手机封面。
  • 默认封面固定使用 R.mipmap.icon_bluetooth_music。

11. 音频焦点流程

蓝牙音乐播放前需要申请音频焦点,否则可能出现手机在播但车机无声或被其他音源压住。

申请焦点:

flowchart TD
    A["准备播放"]
    B["确认 A2DP"]
    C["请求音频焦点"]
    D["解除 A2DP 静音"]
    E["允许播放命令"]

    A --> B
    B --> C
    C --> D
    D --> E

失败规则:

失败点 结果
A2DP 未连接 requestFocus 返回 false
音频焦点申请失败 不应认为蓝牙音乐已经有效播放
MediaSession 未就绪 AVRCP 控制可能无效

焦点变化:

flowchart TD
    A["焦点变化"]
    B["静音或恢复 A2DP"]
    C["暂停或恢复播放"]
    D["刷新音乐状态"]

    A --> B
    B --> C
    C --> D

焦点变化处理:

焦点事件 当前处理
AUDIOFOCUS_GAIN 解除 A2DP 静音;如果丢焦前在播,则恢复播放
AUDIOFOCUS_LOSS 释放焦点;暂停蓝牙音乐;清空 Launcher 音乐信息
AUDIOFOCUS_LOSS_TRANSIENT A2DP 静音;记录是否正在播放;暂停
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 降低 A2DP 音量

12. 断开与关闭流程

flowchart TD
    A["连接断开"]
    B["SDK 收到回调"]
    C["清空音乐缓存"]
    D["通知 UI 未连接"]
    E["显示未连接面板"]

    A --> B
    B --> C
    C --> D
    D --> E

相关回调:

  • onDisconnect
  • onA2dpDisconnect
  • onClose
  • onBtDisconnect
  • onBtClose

清理内容:

清理项 目的
title / artist / album 避免断开后继续显示旧歌曲
progress / duration 避免进度条停在旧状态
播放状态 播放按钮恢复为播放图标
mLiveDataA2dpConnectStatus = false UI 切回未连接面板

13. 当前实现中的关键状态

状态 位置 含义
mLastPlayState BtMusicFragment / BluetoothMusicActivity 记录上次播放状态,用于页面恢复时决定是否续播
musicPlaying BtMusicViewModel 音频焦点丢失前是否在播放
mMusicInfo BtMusicViewModel 最近一次音乐信息缓存
mLiveDataMusicInfo BtMusicViewModel 歌曲信息 UI 通知
mLiveDataPlayState BtMusicViewModel 播放状态 UI 通知
mLiveDataA2dpConnectStatus BtMusicViewModel A2DP 连接状态 UI 通知
isFocus BtMusicAudioManager 蓝牙音乐是否持有音频焦点
isPlayingLossFocus BtMusicAudioManager 丢失焦点前是否在播放

14. 和老项目对齐的现状

已对齐:

  • 主界面增加“蓝牙音乐”入口。
  • 蓝牙音乐 UI 使用老项目默认封面和圆形控制按钮。
  • 进度条资源复用老项目。
  • 封面固定使用默认蓝牙音乐图,不使用手机专辑图。
  • 歌名、歌手、进度、时间通过现有 SDK 刷新。
  • 播放、暂停、上一首、下一首继续走新项目 SDK,不搬旧项目业务代码。

未完全补齐:

  • 未连接状态下 tv_connect_btn 目前没有跳转蓝牙设置页。
  • 没有“连接中”状态,例如 A2DP 正在连接时的进度提示。
  • MediaBrowser 未连接时没有明显错误提示。
  • 播放控制失败没有 UI 反馈。
  • Fragment 与 Activity 两套蓝牙音乐逻辑重复,后续可抽公共逻辑。

15. 开发切入点

15.1 要改 UI

优先看:

  • fragment_bt_music.xml
  • activity_bluetooth_music.xml
  • selector_music_play.xml
  • selector_music_pause.xml
  • selector_music_pre.xml
  • selector_music_next.xml
  • bt_music_progress_layer.xml

15.2 要改播放控制

优先看:

  • BtMusicFragment.onClick
  • BluetoothMusicActivity.onClick
  • BtMusicViewModel.playBtMusic / pauseBtMusic / preBtMusic / nextBtMusic
  • BtMusicControlManager
  • BtMusicManager
  • BluetoothAvrcpClient

15.3 要改连接逻辑

优先看:

  • BtMusicFragment.btMusicInfo
  • BluetoothMusicActivity.btMusicInfo
  • BtMusicViewModel.connectA2dp
  • BtSettingControlManager.connectA2dp
  • BluetoothA2dpSinkClient

15.4 要改音频焦点

优先看:

  • BtMusicAudioManager.requestAudioFocus
  • BtMusicAudioManager.releaseAudioFocus
  • BtMusicAudioManager.mAudioFocusListener
  • BtMusicManager.requestFocus

16. 建议补齐的功能

优先级 功能 原因
P0 未连接按钮跳转蓝牙设置页 用户当前只有蓝牙音乐入口,通过设置页跳转时需要闭环
P0 A2DP 连接中状态 避免点击后无反馈
P1 MediaBrowser 未连接提示 手机蓝牙媒体服务未就绪时便于定位
P1 播放控制失败日志/提示 AVRCP 命令可能因手机端播放器未启动而无效
P1 Fragment 和 Activity 逻辑复用 降低后续维护成本
P2 进度条拖动 seek 当前只展示,不支持拖动
P2 专辑图开关 如果后续产品要求展示手机专辑图,可恢复 albumUrl 加载

17. 测试清单

17.1 正常流程

  • 手机已配对并连接 HFP。
  • 进入蓝牙音乐页。
  • 自动检查并连接 A2DP。
  • 手机打开音乐播放器。
  • 点击播放,车机出声。
  • 点击暂停,手机播放器暂停。
  • 点击上一首/下一首,手机播放器切歌。
  • 歌名、歌手、时间、进度刷新。

17.2 连接异常

  • 未连接手机进入蓝牙音乐页,应显示未连接面板。
  • 手机只连 HFP 未连 A2DP,进入音乐页应尝试连接 A2DP。
  • A2DP 连接失败,应能从日志看到 connectA2dp 相关信息。
  • 手机断开蓝牙,UI 应切回未连接状态。

17.3 焦点与打断

  • 蓝牙音乐播放时来电,音乐应暂停或静音。
  • 通话结束后,如果丢焦前在播,应尝试恢复。
  • 切到收音机/USB 音乐,蓝牙音乐应释放或失去焦点。
  • 重新切回蓝牙音乐,按状态恢复播放。

17.4 兼容性

  • 不同手机品牌测试 AVRCP 元数据。
  • 手机音乐 App 未打开时点击播放。
  • 手机音乐 App 开着但无播放队列。
  • 蓝牙断开后快速重连。
  • 系统重启后首次进入蓝牙音乐。

18. 日志关键字

调试时可优先过滤这些关键字:

BtMusicFragment
BluetoothMusicActivity
BtMusicViewModel
BtMusicControlManager
BtMusicManager
BtMusicAudioManager
BluetoothAvrcpClient
MediaBrowserHelper
connectA2dp
onA2dpConnect
onA2dpDisconnect
onPlaybackStateChanged
onMetadataChanged
requestAudioFocus
AUDIOFOCUS_GAIN
AUDIOFOCUS_LOSS

19. 总结

蓝牙音乐模块可以按四条主线理解:

  1. 入口线:MainActivity -> BtMusicFragment。
  2. 连接线:BtMusicFragment -> BtMusicViewModel -> BtSettingControlManager -> A2DP。
  3. 控制线:按钮点击 -> BtMusicViewModel -> BtMusicControlManager -> BtMusicManager -> BluetoothAvrcpClient -> 手机播放器。
  4. 回调线:手机媒体状态 -> MediaBrowserClient -> BluetoothAvrcpClient -> BtMusicManager -> BtMusicViewModel LiveData -> UI。

开发这个模块时,优先保证 A2DP 连接、AVRCP 控制、音频焦点三条链路可验证,再做 UI 微调和异常状态补齐。