first commit

This commit is contained in:
admin committed 2026-11-05 14:35:04 +08:00
commit 669f49b1d6
342 files changed
+320811

No files matched your search

@@ -0,0 +1,98 @@
# 00_总览_一页看懂
## 一句话理解
```text
手机音乐 App 负责播放内容
车机蓝牙音乐模块负责:
1. 接收手机音乐声音
2. 显示歌名、歌手、封面、进度
3. 控制手机播放、暂停、上一曲、下一曲
4. 和收音机、USB 音乐、电话、导航等音源抢焦点
5. 把蓝牙音乐状态同步给 Launcher 卡片
```
## 模块角色
| 角色 | 简单解释 | 关键类 |
| ---------------- | ---------------------- | ------------------------- |
| A2DP Sink | 声音通道,手机把音乐声音推给车机。 | `BluetoothA2dpSinkClient` |
| AVRCP Controller | 控制通道,车机控制手机音乐,也接收歌曲信息。 | `BluetoothAvrcpClient` |
| MediaBrowser | 连接 Android 蓝牙媒体服务。 | `MediaBrowserClient` |
| 音频焦点 | 决定蓝牙音乐是否可以出声。 | `BtMusicAudioManager` |
| 蓝牙音乐核心 | 汇总连接、控制、歌曲信息、焦点。 | `BtMusicManager` |
| 页面 | 给用户看的蓝牙音乐界面。 | `BluetoothMusicActivity` |
| Launcher 卡片 | 桌面小组件显示和控制蓝牙音乐。 | `BluetoothMusicCardView` |
## 总体关系
```text
手机音乐 App
│
├─ 声音流:A2DP
│ ↓
│ BluetoothA2dpSinkClient
│
└─ 控制/歌曲信息:AVRCP
↓
MediaBrowserClient
↓
BluetoothAvrcpClient
↓
BtMusicManager
↓
BtMusicControlManager
↓
BtMusicViewModel
↓
BluetoothMusicActivity
BtMusicManager
├─ 调用 BtMusicAudioManager 管理音频焦点
└─ 调用 BtInfoPushManager 推送给 Launcher
```
## 看代码时的方向
如果你想知道“点击按钮后发生了什么”,从上往下看:
```text
BluetoothMusicActivity
↓
BtMusicViewModel
↓
BtMusicControlManager
↓
BtMusicManager
↓
BluetoothAvrcpClient
```
如果你想知道“歌名歌手从哪里来”,从底层往页面看:
```text
MediaBrowserClient
↓
BluetoothAvrcpClient
↓
BtMusicManager
↓
BtMusicViewModel
↓
BluetoothMusicActivity
```
如果你想知道“Launcher 卡片为什么会刷新”,看:
```text
BluetoothAvrcpClient
↓
BtInfoPushManager
↓
DataManager.putObject(BT_MUSIC_INFO)
↓
Id3Info
↓
BluetoothMusicCardView
```
@@ -0,0 +1,112 @@
# 01_启动初始化流程
## 目标
搞清楚蓝牙音乐模块什么时候初始化、初始化了哪些对象。
## 启动流程
```text
1. 蓝牙 App 进程启动
↓
2. BluetoothApplication.onCreate()
↓
3. 调用父类 BluetoothSdkApplication.onCreate()
↓
4. BluetoothSdkApplication.setPlatform()
↓
5. 初始化各蓝牙业务 Manager
↓
6. BtMusicControlManager.getInstance().init(context)
↓
7. 创建 BtMusicManager
↓
8. BtMusicManager.init(context)
↓
9. 初始化 BluetoothAvrcpClient
↓
10. 初始化 BtMusicAudioManager
↓
11. BluetoothApplication 启动 BluetoothService
```
## 关键源码
### 1. 蓝牙 App 启动
```text
文件:
F:\zdCode\tongli\YX_BlueTooth\src\main\java\com\yaxon\bluetooth\BluetoothApplication.java
核心:
onCreate()
-> super.onCreate()
-> SharedPrefsManager.init()
-> initBluetoothService()
```
### 2. SDK 初始化
```text
文件:
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\BluetoothSdkApplication.java
核心:
setPlatform()
-> BtContactsControlManager.init()
-> BtSettingControlManager.init()
-> BtPhoneControlManager.init()
-> BtCallRecordsControlManager.init()
-> BtMusicControlManager.init()
-> BtVoiceControlManager.init()
```
### 3. 蓝牙音乐初始化
```text
文件:
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\manager\BtMusicControlManager.java
核心:
init(context)
-> new BtMusicManager()
-> mBtMusicManager.init(context)
-> mBtMusicManager.initMusicInfo()
```
### 4. 音乐核心初始化
```text
文件:
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\modules\BtMusicManager.java
核心:
init(context)
-> initData(context)
-> initListener()
initData(context)
-> mMusicInfoBean = new MusicInfo()
-> BluetoothAvrcpClient.getInstance().init(context)
-> BtMusicAudioManager.getInstance().init(context, this)
-> BluetoothA2dpSinkClient.getInstance()
```
## 初始化后已经具备的能力
| 能力 | 是否已准备 |
|---|---|
| 接收 AVRCP 歌曲信息 | 是 |
| 接收 AVRCP 播放状态 | 是 |
| 下发播放/暂停/切歌控制 | 是 |
| 监听 Launcher 控制命令 | 是 |
| 申请/释放音频焦点 | 是 |
| 推送蓝牙音乐信息给 Launcher | 是 |
## 记忆点
```text
蓝牙音乐模块不是进入 BluetoothMusicActivity 才初始化。
只要蓝牙 App 启动,BtMusicManager 和相关 SDK 就已经初始化。
```
@@ -0,0 +1,120 @@
# 02_A2DP连接与断开流程
## A2DP 是什么
```text
A2DP = 蓝牙音乐声音通道
手机音乐 App 播放出来的声音
↓
通过 A2DP
↓
传到车机
```
在这个项目中,车机是 `A2DP Sink`,也就是音频接收端。
## A2DP 连接成功流程
```text
1. 手机和车机建立 A2DP 连接
↓
2. Android 系统发送 BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED
↓
3. BluetoothConnectListenerClient.ConnectStatusReceiver.onReceive()
↓
4. 判断 action 是 A2DP Sink 连接变化
↓
5. setA2dpConnectState(linkedInfo, intent)
↓
6. newState == BluetoothProfile.STATE_CONNECTED
↓
7. onA2dpConnect(linkedInfo)
↓
8. BtMusicManager 收到 onA2dpConnect
↓
9. BtMusicViewModel.onA2dpConnect()
↓
10. mLiveDataA2dpConnectStatus.postValue(true)
↓
11. BluetoothMusicActivity 观察到 true
↓
12. setIsBtConnect(true)
↓
13. 页面显示蓝牙音乐播放界面
```
## A2DP 断开流程
```text
1. 手机断开 A2DP 或蓝牙关闭
↓
2. Android 系统发送 A2DP 断开广播
↓
3. BluetoothConnectListenerClient.setA2dpConnectState()
↓
4. newState == BluetoothProfile.STATE_DISCONNECTED
↓
5. onA2dpDisconnect(linkedInfo)
↓
6. BtMusicManager.clearBtMusic()
↓
7. resetMusicInfo()
↓
8. 通知页面播放状态 false
↓
9. BtMusicViewModel.onA2dpDisconnect()
↓
10. mLiveDataA2dpConnectStatus.postValue(false)
↓
11. BluetoothMusicActivity 显示未连接界面
↓
12. BtInfoPushManager.cleanBtMusic()
↓
13. Launcher 卡片恢复默认状态
```
## 页面进入时的补连逻辑
这个逻辑在 `BluetoothMusicActivity.btMusicInfo()`。
```text
1. 进入蓝牙音乐页
↓
2. BluetoothMusicActivity.onResume()
↓
3. btMusicInfo()
↓
4. getConnectedPhoneDevice()
↓
5. 如果当前有手机设备
↓
6. 判断 A2DP 是否未连接且未处于连接中
↓
7. connectA2dp(device.getAddress())
```
这个逻辑的意义:
```text
如果手机已经和车机有蓝牙关系,但 A2DP 声音通道没有起来,
进入蓝牙音乐页时尝试补连 A2DP。
```
## 关键文件
| 文件 | 重点看 |
|---|---|
| `BluetoothConnectListenerClient.java` | 系统蓝牙连接广播如何分发。 |
| `BluetoothA2dpSinkClient.java` | A2DP 是否连接、连接中、断开中、连接/断开方法。 |
| `BtMusicManager.java` | A2DP 连接/断开后如何清状态、通知页面。 |
| `BtMusicViewModel.java` | 如何把 A2DP 状态转成 LiveData。 |
| `BluetoothMusicActivity.java` | 如何根据 `isBtConnect` 切换界面。 |
## 页面状态
| A2DP 状态 | `isBtConnect` | 页面 |
|---|---|---|
| 已连接 | `true` | 显示蓝牙音乐播放界面 |
| 未连接 | `false` | 显示连接蓝牙提示 |
@@ -0,0 +1,152 @@
# 03_AVRCP歌曲信息与播放状态流程
## AVRCP 是什么
```text
AVRCP = 蓝牙媒体控制协议
它负责两类事情:
1. 车机控制手机:播放、暂停、上一曲、下一曲
2. 手机告诉车机:歌名、歌手、封面、播放状态、进度
```
## MediaBrowser 连接流程
```text
1. BluetoothAvrcpClient.init(context)
↓
2. initMediaBrowserHelper()
↓
3. new MediaBrowserClient(context)
↓
4. MediaBrowserClient 连接系统服务:
com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService
↓
5. 连接成功后创建 MediaControllerCompat
↓
6. 注册 MediaControllerCompat.Callback
↓
7. 可以接收 metadata 和 playbackState
```
## 歌曲信息流程
```text
1. 手机音乐 App 播放或切歌
↓
2. 手机通过 AVRCP 上报 metadata
↓
3. Android 蓝牙服务接收 metadata
↓
4. MediaBrowserClient.mediaControlCallback.onMetadataChanged()
↓
5. BluetoothAvrcpClient.callback.onMetadataChanged()
↓
6. BtMusicManager.onMetadataChanged()
↓
7. 解析字段:
artist
title
album
albumUrl
duration
↓
8. 保存到 MusicInfo
↓
9. 通知 BtMusicViewModel.onPlayMusicInfo()
↓
10. mLiveDataMusicInfo.postValue(musicInfo)
↓
11. BluetoothMusicActivity 更新 UI
```
## 播放状态流程
```text
1. 手机音乐播放状态变化
↓
2. MediaBrowserClient.onPlaybackStateChanged()
↓
3. BluetoothAvrcpClient.onPlaybackStateChanged()
↓
4. BtMusicManager.onPlaybackStateChanged()
↓
5. 判断:
state == PlaybackStateCompat.STATE_PLAYING
↓
6. 更新 MusicInfo.isPlaying
↓
7. 更新 MusicInfo.playPosition
↓
8. 通知 BtMusicViewModel.onPlayMusicState(isPlaying)
↓
9. BluetoothMusicActivity 更新播放按钮图标
```
## 同步给 Launcher 的流程
同样的 AVRCP 数据,还会推给桌面卡片。
```text
BluetoothAvrcpClient.onMetadataChanged()
↓
BtInfoPushManager.sendMusicInfoToLauncher()
↓
转换为 BtMusicInfo
↓
DataManager.putObject(BT_MUSIC_INFO, btInfo)
↓
发送 ACTION_UPDATE_MUSIC_WIDGET
↓
Launcher 卡片刷新
```
播放状态也是类似:
```text
BluetoothAvrcpClient.onPlaybackStateChanged()
↓
BtInfoPushManager.sendMusicStateToLauncher()
↓
DataManager.putObject(BT_MUSIC_INFO, btInfo)
↓
Launcher 卡片刷新
```
## MusicInfo 字段
| 字段 | 含义 | 来源 |
|---|---|---|
| `artist` | 歌手 | `MediaMetadata.METADATA_KEY_ARTIST` |
| `title` | 歌曲名 | `MediaMetadata.METADATA_KEY_TITLE` |
| `album` | 专辑 | `MediaMetadata.METADATA_KEY_ALBUM` |
| `albumUrl` | 封面 | `MediaMetadata.METADATA_KEY_ALBUM_ART_URI` |
| `songLen` | 总时长 | `MediaMetadata.METADATA_KEY_DURATION` |
| `playPosition` | 当前进度 | `PlaybackStateCompat.getPosition()` |
| `isPlaying` | 是否播放 | `PlaybackStateCompat.STATE_PLAYING` |
## Unknown 处理
在 `BtMusicManager.onMetadataChanged()` 中:
```text
如果 artist/title/album 为空、unknow、Not Provided
↓
替换为 strings.xml 中的 unknow 文案
```
在 `BluetoothMusicActivity` 中:
```text
如果 title 为空或等于 unknow
↓
setIsOpenApp(true)
```
含义:
```text
A2DP 可能已经连接,但手机音乐 App 没有提供有效歌曲信息。
页面可能提示用户打开手机音乐 App。
```
@@ -0,0 +1,196 @@
# 04_播放控制与音频焦点流程
## 页面按钮控制流程
```text
1. 用户点击蓝牙音乐页面按钮
↓
2. BluetoothMusicActivity.onClick()
↓
3. 根据按钮 ID 判断动作
↓
4. 调用 BtMusicViewModel
↓
5. BtMusicViewModel 调用 BtMusicControlManager
↓
6. BtMusicControlManager 调用 BtMusicManager
↓
7. BtMusicManager 调用 BluetoothAvrcpClient
↓
8. BluetoothAvrcpClient 调用 MediaController.TransportControls
↓
9. 手机音乐 App 响应
```
## 页面按钮映射
| 控件 ID | 方法 | 最终 AVRCP 控制 |
|---|---|---|
| `iv_bt_music_pre` | `preBtMusic()` | `skipToPrevious()` |
| `iv_bt_music_state` | `playBtMusic()` / `pauseBtMusic()` | `play()` / `pause()` |
| `iv_bt_music_next` | `nextBtMusic()` | `skipToNext()` |
## 播放命令详细链路
```text
BluetoothMusicActivity.onClick()
↓
mViewModel.playBtMusic()
↓
BtMusicViewModel.playBtMusic()
↓
BtMusicControlManager.playBtMusic()
↓
BtMusicManager.requestFocus()
↓
BtMusicAudioManager.requestAudioFocus()
↓
BtMusicManager.playBtMusic()
↓
BluetoothAvrcpClient.play()
↓
TransportControls.play()
```
## 暂停命令详细链路
```text
BluetoothMusicActivity.onClick()
↓
mViewModel.pauseBtMusic()
↓
BtMusicViewModel.pauseBtMusic()
↓
BtMusicControlManager.pauseBtMusic()
↓
BtMusicManager.pauseBtMusic()
↓
BluetoothAvrcpClient.pause()
↓
TransportControls.pause()
```
## 切歌命令详细链路
```text
上一曲:
BluetoothMusicActivity
↓
BtMusicViewModel.preBtMusic()
↓
BtMusicControlManager.preBtMusic()
↓
BtMusicManager.preBtMusic()
↓
BluetoothAvrcpClient.previous()
↓
TransportControls.skipToPrevious()
下一曲:
BluetoothMusicActivity
↓
BtMusicViewModel.nextBtMusic()
↓
BtMusicControlManager.nextBtMusic()
↓
BtMusicManager.nextBtMusic()
↓
BluetoothAvrcpClient.next()
↓
TransportControls.skipToNext()
```
## 音频焦点是什么
```text
音频焦点 = 当前谁有资格出声
车机里可能同时有:
蓝牙音乐
收音机
USB 音乐
视频
导航播报
蓝牙电话
所以蓝牙音乐播放前必须申请焦点。
```
## 焦点申请流程
```text
1. 用户点击播放
↓
2. BtMusicManager.requestFocus()
↓
3. BtMusicAudioManager.requestAudioFocus()
↓
4. AudioManager.requestAudioFocus()
↓
5. 如果返回 AUDIOFOCUS_REQUEST_GRANTED
↓
6. setA2dpUnMute()
↓
7. isFocus = true
↓
8. setMediaButtonEvent()
↓
9. 允许执行 AVRCP play()
```
## 焦点变化处理
| 焦点变化 | 处理 |
|---|---|
| `AUDIOFOCUS_GAIN` | 恢复 A2DP 出声,必要时恢复播放,通知语音当前是蓝牙音乐。 |
| `AUDIOFOCUS_LOSS` | 释放焦点,暂停蓝牙音乐,清空 Launcher 蓝牙音乐信息。 |
| `AUDIOFOCUS_LOSS_TRANSIENT` | 临时失焦,A2DP 静音,暂停蓝牙音乐。 |
| `AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK` | 降低蓝牙音乐音量。 |
## 失焦流程
```text
1. 其他音源抢焦点
↓
2. BtMusicAudioManager.mAudioFocusListener.onAudioFocusChange()
↓
3. 收到 AUDIOFOCUS_LOSS 或 AUDIOFOCUS_LOSS_TRANSIENT
↓
4. 记录当前是否正在播放
↓
5. setA2dpMute()
↓
6. BtMusicManager.pauseBtMusic()
↓
7. 手机音乐暂停
```
## 恢复焦点流程
```text
1. 蓝牙音乐重新获得焦点
↓
2. 收到 AUDIOFOCUS_GAIN
↓
3. isFocus = true
↓
4. setA2dpUnMute()
↓
5. 如果 isPlayingLossFocus == true
↓
6. BtMusicManager.playBtMusic()
↓
7. 手机音乐恢复播放
```
## 注意点
`BluetoothA2dpSinkClient` 中这两个方法底层调用目前被注释:
```text
setFluorideAudioTrackGain()
setFluorideStreaming()
```
所以焦点切换时,A2DP 静音/恢复是否真正生效,需要真机验证。
@@ -0,0 +1,136 @@
# 05_Launcher卡片流程
## Launcher 卡片做什么
Launcher 蓝牙音乐卡片负责:
```text
1. 显示蓝牙音乐标题
2. 显示歌手
3. 显示封面
4. 显示播放状态
5. 显示当前进度和总时长
6. 提供上一曲、播放/暂停、下一曲按钮
```
核心文件:
```text
F:\zdCode\tongli\YX_Launcher\src\main\java\com\yaxon\launcher\card\BluetoothMusicCardView.java
F:\zdCode\tongli\YX_Launcher\src\main\res\layout\card_bluetoothmusic.xml
```
## 卡片显示流程
```text
1. BluetoothAvrcpClient 收到歌曲信息或播放状态
↓
2. BtInfoPushManager.sendMusicInfoToLauncher()
或 sendMusicStateToLauncher()
↓
3. 组装 BtMusicInfo
↓
4. DataManager.putObject(BT_MUSIC_INFO, btInfo)
↓
5. 发送 ACTION_UPDATE_MUSIC_WIDGET 广播
↓
6. Launcher 的 Id3Info 读取 BT_MUSIC_INFO
↓
7. Id3Info 回调 BluetoothMusicCardView.onBtMusicInfo()
↓
8. BluetoothMusicCardView.bindInfo()
↓
9. 更新卡片 UI
```
## 卡片显示字段
| 卡片内容 | 数据字段 |
|---|---|
| 歌曲名 | `BtMusicInfo.mTitle` |
| 歌手 | `BtMusicInfo.mArtist` |
| 封面 | `BtMusicInfo.mAlbumUrl` |
| 播放状态 | `BtMusicInfo.mPlayState` |
| 当前进度 | `BtMusicInfo.mPlayPosition` |
| 总时长 | `BtMusicInfo.mSongLen` |
## 卡片进度条逻辑
```text
1. 收到 BtMusicInfo
↓
2. 保存 mLastPositionMs
↓
3. 保存 mLastDurationMs
↓
4. 保存 mLastUpdateUptimeMs
↓
5. 如果是播放中
↓
6. 每秒用本地时间估算进度
↓
7. updateProgress()
```
这意味着:
```text
Launcher 卡片不是每秒都等蓝牙栈回调,
而是在播放中自己按时间推进进度条。
```
## 卡片控制流程
```text
1. 用户点击 Launcher 卡片按钮
↓
2. BluetoothMusicCardView.onClick()
↓
3. notifyBtSourceActive()
↓
4. DataManager.putInt(SOURCE_ID, SOURCE_BT_MUSIC)
↓
5. sendBtControl(cmd)
↓
6. DataManager.putInt(MUSIC_BT_CONTROL_STATE, cmd)
↓
7. BtMusicAudioManager 的 ContentObserver 收到变化
↓
8. 如果蓝牙音乐没有焦点,先 requestAudioFocus()
↓
9. 根据 cmd 调用 BtMusicManager
↓
10. BtMusicManager 调用 BluetoothAvrcpClient
↓
11. 手机音乐 App 响应
↓
12. MUSIC_BT_CONTROL_STATE 重置为 -1
```
## 卡片按钮命令
| 卡片按钮 | 写入命令 |
|---|---|
| 上一曲 | `StateUtils.MUSIC_BT_CONTROL_PRE` |
| 下一曲 | `StateUtils.MUSIC_BT_CONTROL_NEXT` |
| 播放 | `StateUtils.MUSIC_BT_CONTROL_PLAY` |
| 暂停 | `StateUtils.MUSIC_BT_CONTROL_PAUSE` |
## 为什么要重置为 -1
```text
如果连续点击同一个按钮,比如连续点下一曲:
第一次写入 MUSIC_BT_CONTROL_NEXT
↓
ContentObserver 触发
↓
处理完成后写回 -1
↓
下一次再写 MUSIC_BT_CONTROL_NEXT
↓
值发生变化,ContentObserver 可以再次触发
```
如果不重置,同一个值可能不会再次触发观察。
@@ -0,0 +1,194 @@
# 06_开发入口_风险点_测试清单
## 开发入口速查
### 改蓝牙音乐页面
```text
F:\zdCode\tongli\YX_BlueTooth\src\main\res\layout\activity_bluetooth_music.xml
F:\zdCode\tongli\YX_BlueTooth\src\main\java\com\yaxon\bluetooth\main\BluetoothMusicActivity.java
```
常见改动:
```text
改封面
改标题/歌手显示
改进度条
改播放按钮
改未连接页面
增加音效入口
增加跳转蓝牙设置
```
### 改播放控制
```text
F:\zdCode\tongli\YX_BlueTooth\src\main\java\com\yaxon\bluetooth\viewmodel\BtMusicViewModel.java
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\manager\BtMusicControlManager.java
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\modules\BtMusicManager.java
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\platform\BluetoothAvrcpClient.java
```
常见改动:
```text
播放前是否强制申请焦点
暂停后是否释放焦点
切歌时是否清默认封面
页面 onResume 是否自动恢复播放
手机端主动播放时是否允许车机出声
```
### 改 A2DP 连接
```text
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\platform\BluetoothConnectListenerClient.java
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\platform\BluetoothA2dpSinkClient.java
F:\zdCode\tongli\YX_BlueTooth\src\main\java\com\yaxon\bluetooth\viewmodel\BtMusicViewModel.java
```
常见改动:
```text
A2DP 自动连接
A2DP 断开清理
连接中状态判断
未连接页面状态
蓝牙关闭后的数据清理
```
### 改歌曲信息
```text
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\modules\BtMusicManager.java
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\modules\BtInfoPushManager.java
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\model\MusicInfo.java
```
常见改动:
```text
Unknown 兜底文案
封面为空兜底
不同手机音乐 App 兼容
总时长为 0 的处理
播放进度刷新策略
```
### 改 Launcher 卡片
```text
F:\zdCode\tongli\YX_Launcher\src\main\java\com\yaxon\launcher\card\BluetoothMusicCardView.java
F:\zdCode\tongli\YX_Launcher\src\main\res\layout\card_bluetoothmusic.xml
F:\zdCode\tongli\YX_Launcher\src\main\java\com\yaxon\launcher\data\Id3Info.java
```
常见改动:
```text
卡片 UI
卡片点击进入蓝牙音乐页
卡片控制按钮
卡片进度条
卡片默认态
```
### 改音频焦点
```text
F:\zdCode\tongli\YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\audio\BtMusicAudioManager.java
```
常见改动:
```text
蓝牙音乐和收音机抢焦点
蓝牙音乐和 USB 音乐抢焦点
来电暂停/恢复
导航播报降音
媒体按键处理
亿连 A2DP 焦点处理
```
## 当前代码风险点
| 风险 | 文件 | 说明 |
|---|---|---|
| `isA2dpSinkConnected()` 总返回 false | `BtMusicViewModel`、`BtMusicControlManager` | 调了底层但没有 return 结果。 |
| 音效按钮未闭环 | `activity_bluetooth_music.xml` | 有 `iv_sound_effects`,但 Activity 没处理点击。 |
| 连接蓝牙按钮未闭环 | `BluetoothMusicActivity` | `tv_connect_btn` 点击逻辑被注释。 |
| A2DP 静音底层调用被注释 | `BluetoothA2dpSinkClient` | `setFluorideAudioTrackGain()`、`setFluorideStreaming()` 没真正调底层。 |
| 监听移除方法参数疑似错误 | `BtMusicManager` | `removeIBtMusicModuleListener()` 参数是 `IBtSettingModuleListener`。 |
## 真机测试清单
### 连接类
| 用例 | 预期 |
|---|---|
| 手机首次配对并连接 | HFP/A2DP 正常连接,页面显示播放界面。 |
| 只连 HFP 未连 A2DP 后进入蓝牙音乐页 | 页面尝试补连 A2DP。 |
| 播放中断开手机蓝牙 | 页面切未连接,Launcher 清空蓝牙音乐信息。 |
| 蓝牙开关关闭 | 页面切未连接,音乐信息清空。 |
### 播放控制类
| 用例 | 预期 |
|---|---|
| 点击播放 | 申请音频焦点,手机音乐开始播放。 |
| 点击暂停 | 手机音乐暂停,页面按钮切为播放图标。 |
| 点击上一曲 | 手机音乐切上一曲,页面和 Launcher 更新。 |
| 点击下一曲 | 手机音乐切下一曲,页面和 Launcher 更新。 |
| Launcher 点击播放/暂停 | 蓝牙音乐响应,页面和卡片状态同步。 |
### 歌曲信息类
| 用例 | 预期 |
|---|---|
| 普通歌曲 | 显示标题、歌手、封面、进度、总时长。 |
| 标题为空 | 显示 Unknown 或打开手机音乐 App 状态。 |
| 封面为空 | 显示默认封面。 |
| 总时长为 0 | 进度条安全显示,不崩溃。 |
### 音频焦点类
| 用例 | 预期 |
|---|---|
| 蓝牙音乐播放时打开收音机 | 蓝牙音乐暂停或静音,收音机出声。 |
| 收音机播放时手机点蓝牙音乐播放 | 如果蓝牙音乐无焦点,应暂停或不出声。 |
| 蓝牙音乐播放时来电 | 蓝牙音乐暂停,电话优先。 |
| 电话结束 | 根据之前播放状态决定是否恢复蓝牙音乐。 |
## 日志关键词
```text
BtMusicManager
BluetoothAvrcpClient
BluetoothA2dpSinkClient
BtMusicAudioManager
BtInfoPushManager
BluetoothMusicActivity
onPlaybackStateChanged
onMetadataChanged
requestAudioFocus
AUDIOFOCUS_GAIN
AUDIOFOCUS_LOSS
onA2dpConnect
onA2dpDisconnect
MUSIC_BT_CONTROL_STATE
```
## 排查顺序
```text
1. 先看是否 A2DP connected
2. 再看 MediaBrowserClient 是否连接成功
3. 再看是否收到 onMetadataChanged
4. 再看是否收到 onPlaybackStateChanged
5. 再看点击播放后是否 requestAudioFocus 成功
6. 再看是否调用 BluetoothAvrcpClient.play/pause/next/previous
7. 再看 Launcher 是否写入 MUSIC_BT_CONTROL_STATE
8. 最后看 BtInfoPushManager 是否写入 BT_MUSIC_INFO
```
@@ -0,0 +1,748 @@
# 蓝牙音乐模块功能流程梳理
更新时间: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 分层架构图
图里只保留层级关系,详细类名放到下方表格,避免图过大。
```mermaid
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 播放控制链路
用户点按钮时,命令从上往下走,最后控制手机播放器。
```mermaid
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。
```mermaid
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 音频通道:
```mermaid
flowchart TB
A["进入音乐页"]
B["检查 A2DP"]
C["连接手机 A2DP"]
D["音频通道就绪"]
E["车机扬声器输出"]
A --> B
B --> C
C --> D
D --> E
```
再看音频焦点:
```mermaid
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 初始化在应用启动时完成。
```mermaid
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`。
```mermaid
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` 控制两种状态。
```mermaid
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 是手机音乐音频传到车机的通道。
进入页面后的检查:
```mermaid
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`
连接结果回调:
```mermaid
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 点击播放 / 暂停
```mermaid
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 点击上一首 / 下一首
```mermaid
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 回传媒体状态。
播放状态回调:
```mermaid
flowchart TD
A["手机播放状态变化"]
B["系统媒体回调"]
C["SDK 转换状态"]
D["LiveData 通知"]
E["刷新按钮和进度"]
A --> B
B --> C
C --> D
D --> E
```
歌曲信息回调:
```mermaid
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. 音频焦点流程
蓝牙音乐播放前需要申请音频焦点,否则可能出现手机在播但车机无声或被其他音源压住。
申请焦点:
```mermaid
flowchart TD
A["准备播放"]
B["确认 A2DP"]
C["请求音频焦点"]
D["解除 A2DP 静音"]
E["允许播放命令"]
A --> B
B --> C
C --> D
D --> E
```
失败规则:
| 失败点 | 结果 |
|---|---|
| A2DP 未连接 | `requestFocus` 返回 `false` |
| 音频焦点申请失败 | 不应认为蓝牙音乐已经有效播放 |
| MediaSession 未就绪 | AVRCP 控制可能无效 |
焦点变化:
```mermaid
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. 断开与关闭流程
```mermaid
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. 日志关键字
调试时可优先过滤这些关键字:
```text
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 微调和异常状态补齐。
@@ -0,0 +1,84 @@
# 蓝牙音乐模块流程与开发指南
> 这是入口文档。
> 本次排版已去掉 Mermaid 图,改为“纯文本流程块 + 编号步骤表 + 拆分文档”,适合 Obsidian 直接阅读。
## 推荐阅读顺序
| 顺序 | 文档 | 先看什么 |
|---|---|---|
| 1 | [[00_总览_一页看懂]] | 先建立模块整体印象。 |
| 2 | [[01_启动初始化流程]] | 看蓝牙音乐 SDK 是怎么启动的。 |
| 3 | [[02_A2DP连接与断开流程]] | 看“有没有声音通道”怎么判断。 |
| 4 | [[03_AVRCP歌曲信息与播放状态流程]] | 看歌名、歌手、播放状态从哪里来。 |
| 5 | [[04_播放控制与音频焦点流程]] | 看播放、暂停、切歌、焦点抢占。 |
| 6 | [[05_Launcher卡片流程]] | 看桌面卡片怎么显示和控制蓝牙音乐。 |
| 7 | [[06_开发入口_风险点_测试清单]] | 开发前定位文件、风险点和测试项。 |
## 最短主链路
### 用户点击播放
```text
用户
↓
BluetoothMusicActivity.onClick()
↓
BtMusicViewModel.playBtMusic()
↓
BtMusicControlManager.playBtMusic()
↓
BtMusicManager.requestFocus()
↓
BtMusicAudioManager.requestAudioFocus()
↓
BtMusicManager.playBtMusic()
↓
BluetoothAvrcpClient.play()
↓
MediaControllerCompat.TransportControls.play()
↓
手机音乐 App 开始播放
```
### 手机切歌后刷新页面
```text
手机音乐 App 切歌
↓
Android 蓝牙栈 AVRCP 元数据变化
↓
MediaBrowserClient.onMetadataChanged()
↓
BluetoothAvrcpClient.onMetadataChanged()
↓
BtMusicManager 解析歌曲信息
↓
BtMusicViewModel.mLiveDataMusicInfo
↓
BluetoothMusicActivity 更新歌名/歌手/封面/时长
↓
BtInfoPushManager.sendMusicInfoToLauncher()
↓
Launcher 蓝牙音乐卡片更新
```
## 当前代码根路径
```text
F:\zdCode\tongli
```
## 蓝牙音乐最核心文件
| 文件 | 作用 |
|---|---|
| `YX_BlueTooth\src\main\java\com\yaxon\bluetooth\main\BluetoothMusicActivity.java` | 蓝牙音乐页面。 |
| `YX_BlueTooth\src\main\java\com\yaxon\bluetooth\viewmodel\BtMusicViewModel.java` | 页面状态和控制转发。 |
| `YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\manager\BtMusicControlManager.java` | 对外音乐控制入口。 |
| `YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\modules\BtMusicManager.java` | 蓝牙音乐核心业务。 |
| `YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\platform\BluetoothA2dpSinkClient.java` | A2DP 音频连接。 |
| `YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\platform\BluetoothAvrcpClient.java` | AVRCP 控制和歌曲信息。 |
| `YX_BluetoothSDK\src\main\java\com\yaxon\bluetoothsdk\audio\BtMusicAudioManager.java` | 音频焦点和媒体按键。 |
| `YX_Launcher\src\main\java\com\yaxon\launcher\card\BluetoothMusicCardView.java` | Launcher 蓝牙音乐卡片。 |