Android 不同的蓝牙音箱连接后声音突变问题分析解决
文章目录
- 一、前言
- 二、问题现象
- 三、原因分析
- [1、Android 音量体系概述](#1、Android 音量体系概述)
- 2、蓝牙设备连接时的音量设置链路
- 3、音量范围与内部表示
- [4、Settings.System 音量属性说明](#4、Settings.System 音量属性说明)
- 5、问题根因定位
- 四、解决方案
- [1、AdiDeviceState 添加音量持久化字段](#1、AdiDeviceState 添加音量持久化字段)
- [2、AudioDeviceInventory 连接时恢复音量 + 保护窗口](#2、AudioDeviceInventory 连接时恢复音量 + 保护窗口)
- [3、AudioDeviceBroker 新增调度方法](#3、AudioDeviceBroker 新增调度方法)
- [4、AudioService 音量变化时保存 + 同步 + 拦截](#4、AudioService 音量变化时保存 + 同步 + 拦截)
- 五、踩坑记录
- [1、编译报错:cannot find symbol variable address](#1、编译报错:cannot find symbol variable address)
- 2、新设备首次连接音量变最大
- 3、保护窗口未覆盖所有连接分支
- [4、AVRCP 绝对音量瞬间突变](#4、AVRCP 绝对音量瞬间突变)
- 六、调试验证
- 七、其他
一、前言
在 Android 设备上使用蓝牙音箱时,发现一个影响用户体验的问题:
连接蓝牙音箱 A 并调节到合适音量后,断开再连接蓝牙音箱 B,音箱 B 的音量会突变为最大值,声音非常大。
再次连接音箱 A 时,之前调节好的音量也丢失了。
多次切换蓝牙音响设备后,有概率声音会突变成最大值。
经过分析,问题由三个因素叠加导致:
- 音量按设备类型存储,不按设备地址存储 --- 所有 A2DP 蓝牙设备共享同一个音量值
- AVRCP 绝对音量覆盖 --- 蓝牙栈连接后上报设备自身音量(通常是最大值),覆盖系统侧音量
- 多线程竞争 --- AVRCP 音量上报在独立线程执行,不受设备连接锁保护,导致瞬间音量突变
本文记录完整的分析过程、多轮踩坑和最终解决方案。
涉及的代码基于 Android 15(AOSP),核心修改在 frameworks/base/services/core/java/com/android/server/audio/ 目录下。
其实这个问题是正常的,Android系统默认是启动蓝牙绝对音量,不同的蓝牙设备会有不同的音量大小;
比如:
系统本身音量40%,连接蓝牙A,变成80%,断开蓝牙系统音量显示40%,再连接蓝牙音量会显示80;
连接蓝牙后修改音量大小,音量值大小默认值保存到蓝牙音箱设备的,
这时候断开蓝牙设备,系统音量值还是会显示40%音量。
如果实在要适配成:
比如 :设备音量为50%,不管切换连接多个蓝牙音量都是50%,或者断开蓝牙设备,查看音量都是50%;
后续:不管是在蓝牙连接还是不连接的情况下,调节音量大小后,连不连接蓝牙,音量大小都是记忆的;
可以参考下面的代码适配,但是也可能会导致一些音量大小问题。
二、问题现象
操作步骤:
- 连接蓝牙音箱 A,调节音量到 50%
- 断开音箱 A,连接蓝牙音箱 B
- 音箱 B 音量突变为最大值(100%)
| 场景 | 预期行为 | 实际行为 |
|---|---|---|
| 首次连接音箱 A,调到 50% | 音量 50% | ✅ 正常 |
| 断开 A,连接音箱 B | 使用默认音量或上次音量 | ❌ 音量突变为最大值 |
| 断开 B,重新连接音箱 A | 恢复之前的 50% | ❌ 音量不确定,可能是最大值 |
| 多次快速切换 A/B | 各自恢复保存的音量 | ❌ 有概率突变为最大值 |
实际日志中的关键信息:
# 音箱A连接,音量正常恢复为500(内部表示,对应50%)
AS.AudioDeviceInventory: A2DP restored savedVolume=500 dev=0x80 addr=48:F3:F3:ED:78:AD
# 音箱B连接,savedVolume=1000(最大值!)
AS.AudioDeviceInventory: A2DP restored savedVolume=1000 dev=0x80 addr=41:42:1E:54:F0:7C
# 蓝牙栈通过AVRCP上报最大音量(在另一个线程)
AS.AudioService: onSetStreamVolume stream=0 index=1000 dev=0x80 caller=com.android.bluetooth
# AVRCP绝对音量推到最大
AS.AudioDeviceBroker: AVRCP absVolIndex=15
# 锁竞争导致保护延迟
system_server: Long monitor contention with owner AudioDeviceBroker (1093) at
AudioDeviceInventory.onSetBtActiveDevice waiters=0 in
AudioDeviceInventory.updateBtDeviceVolumeIndex for 354ms
三、原因分析
1、Android 音量体系概述
Android 音频系统中,音量管理涉及以下几个核心类:
| 类 | 文件路径 | 职责 |
|---|---|---|
AudioService |
frameworks/base/services/core/.../AudioService.java |
音量控制总入口,管理 VolumeStreamState |
AudioDeviceBroker |
同上目录 AudioDeviceBroker.java |
设备状态调度中间层 |
AudioDeviceInventory |
同上目录 AudioDeviceInventory.java |
已连接设备清单管理,蓝牙设备连接/断开处理 |
AdiDeviceState |
同上目录 AdiDeviceState.java |
设备持久化状态(空间音频、设备类别等) |
音量存储的核心机制:
VolumeStreamState(AudioService 内部类)
├── mIndexMap: SparseIntArray // key=设备类型(int), value=音量值(内部表示)
├── mStreamType: int // 流类型(如 STREAM_MUSIC = 3)
└── 持久化到 Settings.System // key 格式: "volume_music_bt_a2dp"
关键问题:mIndexMap 的 key 是设备类型(如 DEVICE_OUT_BLUETOOTH_A2DP = 0x80),不是设备地址。
这意味着所有 A2DP 蓝牙设备共享同一个音量值。
这个并不是所有蓝牙音箱设备是同一个相等的值,只是切换连接上不同蓝牙后会更新使用这个对象值。
2、蓝牙设备连接时的音量设置链路
蓝牙音箱连接时的完整调用链路:
蓝牙栈通知设备连接
↓
AudioDeviceBroker.queueOnBluetoothActiveDeviceChanged()
↓ (线程: AudioDeviceBroker)
AudioDeviceInventory.onSetBtActiveDevice() ← 持有 mDevicesLock
↓ A2DP 分支
├── postSetVolumeIndexOnDevice(btInfo.mVolume * 10) ← 设置蓝牙上报的音量
├── makeA2dpDeviceAvailable()
│ ├── setDeviceConnectionState() → 通知 AudioPolicy
│ ├── postAccessoryPlugMediaUnmute()
│ └── addAudioDeviceInInventoryIfNeeded()
↓
蓝牙栈通过 AVRCP 上报绝对音量(另一个线程,不需要 mDevicesLock)
↓
AudioService.setStreamVolume() → FLAG_BLUETOOTH_ABS_VOLUME
↓
onSetStreamVolume() → setStreamVolumeInt() → setDeviceVolume()
↓
postSetAvrcpAbsoluteVolumeIndex(index / 10) → 下发到蓝牙设备
onSetBtActiveDevice() 中 A2DP 分支的原始代码:
java
// AudioDeviceInventory.java - onSetBtActiveDevice() 原始代码
case BluetoothProfile.A2DP:
if (switchToAvailable) {
if (btInfo.mVolume != -1) {
mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC,
btInfo.mVolume * 10, btInfo.mAudioSystemDevice,
"onSetBtActiveDevice");
}
makeA2dpDeviceAvailable(btInfo, codec, "onSetBtActiveDevice");
}
这里只使用了蓝牙栈上报的 btInfo.mVolume,没有任何按设备地址恢复音量的逻辑。
3、音量范围与内部表示
Android 音频系统中存在三套不同的音量范围:
| 层级 | 范围 | 定义位置 | 说明 |
|---|---|---|---|
| 蓝牙 AVRCP 协议 | 0~127 | 蓝牙规范 | 7-bit,蓝牙栈内部转换 |
| 用户可见 / UI | 0~15(默认) | AudioService.MAX_STREAM_VOLUME |
可通过 ro.config.media_vol_steps 修改 |
| VolumeStreamState 内部 | 用户可见 ×10 | AudioService.java VolumeStreamState 构造函数 |
getIndex() 返回此值 |
java
// AudioService.java - MAX_STREAM_VOLUME 定义
protected static int[] MAX_STREAM_VOLUME = new int[] {
5, // STREAM_VOICE_CALL
7, // STREAM_SYSTEM
7, // STREAM_RING
15, // STREAM_MUSIC ← 默认最大15,可通过系统属性修改
7, // STREAM_ALARM
...
};
// VolumeStreamState 构造函数中 ×10 转换
mIndexMin = MIN_STREAM_VOLUME[streamType] * 10; // 0
mIndexMax = MAX_STREAM_VOLUME[streamType] * 10; // 150(默认)或 1000(自定义100步)
蓝牙栈在上报音量时已完成 AVRCP(0~127)到用户可见值的转换,
btInfo.mVolume 的范围就是用户可见值(0~15 或自定义),代码中 btInfo.mVolume * 10 转成内部表示。
我们保存和恢复用的都是 VolumeStreamState 的内部表示(×10 后的值),链路一致,不存在转换差。
4、Settings.System 音量属性说明
通过 settings list system | grep volume 可以看到持久化的音量值:
volume_alarm=6
volume_bluetooth_sco=7
volume_music=5
volume_music_bt_a2dp=71
volume_music_speaker=29
volume_notification=5
volume_ring=5
volume_system=7
volume_voice=4
key 的生成逻辑在 VolumeStreamState.getSettingNameForDevice() 中:
java
// 基础名 = VOLUME_SETTINGS_INT[streamType],如 "volume_music"
// 设备后缀 = AudioSystem.getOutputDeviceName(device),如 "bt_a2dp"、"speaker"
// 最终 key = "volume_music" + "_" + "bt_a2dp" = "volume_music_bt_a2dp"
各属性说明:
| Settings 属性 | 流类型 | 设备类型 | 说明 |
|---|---|---|---|
volume_music=5 |
STREAM_MUSIC | DEVICE_OUT_DEFAULT | 媒体音量默认值 |
volume_music_bt_a2dp=71 |
STREAM_MUSIC | DEVICE_OUT_BLUETOOTH_A2DP (0x80) | 蓝牙A2DP媒体音量,所有A2DP设备共享 |
volume_music_speaker=29 |
STREAM_MUSIC | DEVICE_OUT_SPEAKER (0x2) | 扬声器媒体音量 |
volume_alarm=6 |
STREAM_ALARM | 默认设备 | 闹钟音量 |
volume_bluetooth_sco=7 |
STREAM_BLUETOOTH_SCO | 默认设备 | 蓝牙通话音量 |
volume_notification=5 |
STREAM_NOTIFICATION | 默认设备 | 通知音量 |
volume_ring=5 |
STREAM_RING | 默认设备 | 铃声音量 |
volume_system=7 |
STREAM_SYSTEM | 默认设备 | 系统提示音音量 |
volume_voice=4 |
STREAM_VOICE_CALL | 默认设备 | 通话音量 |
volume_music_bt_a2dp 就是问题核心 --- 所有 A2DP 蓝牙设备共享这一个值。
其实主要就是看 volume_music_bt_a2dp 和 volume_music_speaker 的值;
正常情况下,连接蓝牙设置音量大小调节的就是 volume_music_bt_a2dp 的值;
不连接蓝牙音箱的情况下,音量大小的是就是 volume_music_speaker 的值,调节音量也是改变这个值;
所以只要保证这两个值任何时候完全一致,就能保证音量大小不变了。但是实际没那么容易。
5、问题根因定位
问题由三个因素叠加导致:
因素一:音量按设备类型存储,不按设备地址存储
VolumeStreamState.mIndexMap 的 key 是 DEVICE_OUT_BLUETOOTH_A2DP(0x80),
所有 A2DP 蓝牙设备共享同一个音量值。切换设备时无法恢复各自的音量。
AdiDeviceState 虽然按设备地址区分设备,但原始代码只保存了空间音频、头部追踪、设备类别等信息,
没有音量字段。
因素二:AVRCP 绝对音量覆盖
蓝牙音箱连接后,蓝牙栈通过 AVRCP 协议上报设备自身的音量(通常是最大值 127,转换后对应系统最大音量),
通过 onSetStreamVolume() 设置到系统中,覆盖了之前的音量。
因素三:多线程竞争导致瞬间突变
这是最隐蔽的问题。从日志分析发现:
线程1093 (AudioDeviceBroker): onSetBtActiveDevice() 持有 mDevicesLock
→ 恢复 savedVolume=700
→ makeA2dpDeviceAvailable() (耗时较长)
线程1529 (Binder): 蓝牙栈调用 onSetStreamVolume(index=1000)
→ 不需要 mDevicesLock,直接修改 VolumeStreamState
→ 音量瞬间变为最大值!
→ UI 显示 STREAM_MUSIC 100
线程1096 (AudioHandler): setDeviceVolume(index=700)
→ 约1秒后恢复正确音量
onSetStreamVolume 在独立线程执行,不受 mDevicesLock 保护,
直接修改了 VolumeStreamState 的 index,导致约 1 秒的音量突变窗口。
用户听到的"声音变最大"就是这个窗口期。
四、解决方案
整体思路:
- 在
AdiDeviceState中新增音量字段,实现按设备地址记忆音量 - 蓝牙连接时恢复保存的音量,无保存记录时同步扬声器音量
- 音量变化时保存到对应设备的
AdiDeviceState,并同步到扬声器 - 通过保护窗口机制,阻止蓝牙栈 AVRCP 音量覆盖(包括
onSetStreamVolume入口拦截)
完整的数据流:
用户调音量 → AudioService.setDeviceVolume()
→ [新增] 同步到 DEVICE_OUT_SPEAKER
→ [新增] AudioDeviceBroker.postPersistBtDeviceVolume()
→ [新增] AudioDeviceInventory.updateBtDeviceVolumeIndex()
→ [新增] AdiDeviceState.setVolumeIndex()
→ postPersistAudioDeviceSettings() → 持久化到 Settings.Secure
蓝牙重连 → AudioDeviceInventory.onSetBtActiveDevice()
→ [新增] 激活保护窗口
→ [新增] 从 AdiDeviceState 读取 savedVolumeIndex
→ postSetVolumeIndexOnDevice() → 恢复音量
蓝牙栈 AVRCP 上报 → AudioService.onSetStreamVolume()
→ [新增] 检查保护窗口,caller=com.android.bluetooth 时拦截
→ return(不执行音量设置)
1、AdiDeviceState 添加音量持久化字段
文件路径:frameworks/base/services/core/java/com/android/server/audio/AdiDeviceState.java
新增字段和方法:
java
// 新增字段,-1 表示未设置过音量
private int mVolumeIndex = -1;
public synchronized void setVolumeIndex(int volumeIndex) {
mVolumeIndex = volumeIndex;
}
public synchronized int getVolumeIndex() {
return mVolumeIndex;
}
修改 toPersistableString(),追加音量字段:
java
// 修改后(末尾追加 mVolumeIndex)
public synchronized String toPersistableString() {
return (new StringBuilder().append(mDeviceType)
.append(SETTING_FIELD_SEPARATOR).append(mDeviceAddress)
.append(SETTING_FIELD_SEPARATOR).append(mSAEnabled ? "1" : "0")
.append(SETTING_FIELD_SEPARATOR).append(mHasHeadTracker ? "1" : "0")
.append(SETTING_FIELD_SEPARATOR).append(mHeadTrackerEnabled ? "1" : "0")
.append(SETTING_FIELD_SEPARATOR).append(mInternalDeviceType)
.append(SETTING_FIELD_SEPARATOR).append(mAudioDeviceCategory)
.append(SETTING_FIELD_SEPARATOR).append(mVolumeIndex) // 新增
.toString());
}
修改 fromPersistedString(),兼容新字段:
java
// 上限从7改为8
if (fields.length < 5 || fields.length > 8) {
return null;
}
// 在 setAudioDeviceCategory 之后新增
if (fields.length >= 8) {
deviceState.setVolumeIndex(Integer.parseInt(fields[7]));
}
修改 getPeristedMaxSize():
java
// 修改后(音量索引最多4字符 + 1分隔符 = +5)
public static int getPeristedMaxSize() {
return 44;
}
同步修改 equals()、hashCode()、toString() 加入 mVolumeIndex。
2、AudioDeviceInventory 连接时恢复音量 + 保护窗口
文件路径:frameworks/base/services/core/java/com/android/server/audio/AudioDeviceInventory.java
2.1 新增成员变量和保护窗口方法
java
/** Protection window in ms to ignore volume write-back right after BT device connection */
private static final long BT_VOLUME_PROTECTION_WINDOW_MS = 5000;
/** Timestamp of the last BT device connection */
private volatile long mLastBtConnectTimeMs = 0;
/**
* Returns true if the BT volume protection window is currently active.
* Used by AudioService to block AVRCP absolute volume from overwriting
* the restored per-device volume during connection.
*/
boolean isBtVolumeProtectionActive() {
return (System.currentTimeMillis() - mLastBtConnectTimeMs)
< BT_VOLUME_PROTECTION_WINDOW_MS;
}
2.2 修改 onSetBtActiveDevice() A2DP 分支
java
// 修改后
case BluetoothProfile.A2DP:
if (switchToAvailable) {
int savedVolume = -1;
AdiDeviceState ads = findBtDeviceStateForAddress(
address, btInfo.mAudioSystemDevice);
if (ads != null) {
savedVolume = ads.getVolumeIndex();
}
// 所有连接场景都激活保护窗口
mLastBtConnectTimeMs = System.currentTimeMillis();
if (savedVolume != -1) {
// 有保存记录,恢复该设备的音量(含范围校验)
int maxIndex = mDeviceBroker.getMaxVssVolumeForStream(
AudioSystem.STREAM_MUSIC);
if (savedVolume > maxIndex) {
savedVolume = maxIndex;
}
mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC,
savedVolume, btInfo.mAudioSystemDevice,
"onSetBtActiveDevice_restored");
} else {
// 无保存记录,同步扬声器音量
int speakerVolume = mDeviceBroker.getVssVolumeForDevice(
AudioSystem.STREAM_MUSIC,
AudioSystem.DEVICE_OUT_SPEAKER);
mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC,
speakerVolume, btInfo.mAudioSystemDevice,
"onSetBtActiveDevice_syncSpeaker");
}
makeA2dpDeviceAvailable(btInfo, codec, "onSetBtActiveDevice");
}
关键点:mLastBtConnectTimeMs 在 if/else 之前设置,确保所有连接场景都激活保护窗口。
2.3 修改 makeLeAudioDeviceAvailable() 音量恢复
java
// 修改后
// 所有连接场景都激活保护窗口
mLastBtConnectTimeMs = System.currentTimeMillis();
final int leAudioVolIndex;
if (savedVolume != -1) {
int maxIndex = mDeviceBroker.getMaxVssVolumeForStream(streamType);
if (savedVolume > maxIndex) {
savedVolume = maxIndex;
}
leAudioVolIndex = savedVolume;
} else {
// 无保存记录,同步扬声器音量
int speakerVolume = mDeviceBroker.getVssVolumeForDevice(
streamType, AudioSystem.DEVICE_OUT_SPEAKER);
leAudioVolIndex = speakerVolume;
}
注意:makeLeAudioDeviceAvailable() 中 address 变量定义在 if (device != AudioSystem.DEVICE_NONE) 块内,
外层的 if (btInfo.mIsLeOutput) 块访问不到,必须用 btInfo.mDevice.getAddress() 获取地址。
2.4 新增 updateBtDeviceVolumeIndex() 方法
java
void updateBtDeviceVolumeIndex(int device, int volumeIndex) {
if (!isBluetoothOutDevice(device)) {
return;
}
// 保护窗口内跳过保存
if (isBtVolumeProtectionActive()) {
return;
}
synchronized (mDevicesLock) {
for (DeviceInfo di : mConnectedDevices.values()) {
if (di.mDeviceType == device) {
AdiDeviceState ads = findBtDeviceStateForAddress(
di.mDeviceAddress, device);
if (ads != null) {
ads.setVolumeIndex(volumeIndex);
}
break;
}
}
}
}
3、AudioDeviceBroker 新增调度方法
文件路径:frameworks/base/services/core/java/com/android/server/audio/AudioDeviceBroker.java
java
/**
* Saves the volume index for a connected Bluetooth device and triggers persistence.
*/
/*package*/ void postPersistBtDeviceVolume(int device, int volumeIndex) {
mDeviceInventory.updateBtDeviceVolumeIndex(device, volumeIndex);
postPersistAudioDeviceSettings();
}
/**
* Returns true if the BT volume protection window is currently active.
*/
/*package*/ boolean isBtVolumeProtectionActive() {
return mDeviceInventory.isBtVolumeProtectionActive();
}
4、AudioService 音量变化时保存 + 同步 + 拦截
文件路径:frameworks/base/services/core/java/com/android/server/audio/AudioService.java
4.1 onSetStreamVolume() 入口拦截蓝牙栈音量
这是解决"瞬间音量突变"的关键修改:
java
/*package*/ void onSetStreamVolume(int streamType, int index, int flags, int device,
String caller, boolean hasModifyAudioSettings, boolean canChangeMute) {
// 保护窗口内阻止蓝牙栈的音量设置
if (AudioSystem.isBluetoothOutDevice(device)
&& "com.android.bluetooth".equals(caller)
&& mDeviceBroker.isBtVolumeProtectionActive()) {
Log.i(TAG, "blocking BT stack volume change during protection window");
return;
}
// ... 原有逻辑 ...
}
4.2 setDeviceVolume() 保存蓝牙音量 + 同步扬声器
java
/*package*/ void setDeviceVolume(VolumeStreamState streamState, int device) {
// ... 原有的音量应用和持久化逻辑 ...
// 新增:蓝牙设备音量变化时保存到 AdiDeviceState 并同步到扬声器
if (AudioSystem.isBluetoothOutDevice(device)
&& streamState.mStreamType == AudioSystem.STREAM_MUSIC) {
int btVolIndex = streamState.getIndex(device);
mDeviceBroker.postPersistBtDeviceVolume(device, btVolIndex);
// 同步到扬声器,保持 volume_music_speaker 和 volume_music_bt_a2dp 一致
int speakerIndex = streamState.getIndex(AudioSystem.DEVICE_OUT_SPEAKER);
if (speakerIndex != btVolIndex) {
setStreamVolumeInt(streamState.mStreamType, btVolIndex,
AudioSystem.DEVICE_OUT_SPEAKER, true /*force*/,
"syncBtVolumeToSpeaker", true /*hasModifyAudioSettings*/);
}
}
}
五、踩坑记录
1、新设备首次连接音量变最大
现象: 第二个蓝牙音箱首次连接时,savedVolume=1000(最大值),但该设备从未保存过音量。
原因分析:
updateBtDeviceVolumeIndex() 通过遍历 mConnectedDevices 按设备类型匹配。
当第一个音箱断开、第二个音箱连接后,系统因设备切换重新应用音量(setDeviceVolume 被调用),
此时 VolumeStreamState 中 DEVICE_OUT_BLUETOOTH_A2DP 的 index 已被蓝牙栈通过 AVRCP 设为最大值 1000,
这个值被保存到了第二个音箱的 AdiDeviceState 中。
解决: 引入保护窗口机制,连接后 5 秒内忽略音量回写。
同时,无保存记录的新设备改为同步扬声器音量,而不是使用蓝牙栈上报值。
2、保护窗口未覆盖所有连接分支
现象: 设备上运行旧代码时,btInfo.mVolume 分支不会设置 mLastBtConnectTimeMs,保护窗口未激活。
日志证据:
A2DP btVolume=500 ← 走了 btInfo.mVolume 分支
saving BT volIndex=1000 ← 保护窗口没生效,AVRCP 最大值被保存
原因: mLastBtConnectTimeMs 最初只在 savedVolume != -1 分支中设置。
解决: 将 mLastBtConnectTimeMs = System.currentTimeMillis() 提到 if/else 之前,
确保所有连接场景(有保存记录、无保存记录)都激活保护窗口。
3、AVRCP 绝对音量瞬间突变
现象: 多次切换蓝牙音响后,有概率声音瞬间变为最大值,约 1 秒后恢复。
日志证据:
15:41:53.767 A2DP restored savedVolume=700 (线程1093, 持有mDevicesLock)
15:41:54.103 onSetStreamVolume index=1000 caller=com.android.bluetooth (线程1529)
15:41:54.109 AVRCP absVolIndex=15
15:41:54.119 level_changed STREAM_MUSIC 100 ← UI显示最大值!
15:41:54.120 Long monitor contention...updateBtDeviceVolumeIndex for 354ms
15:41:55.080 setDeviceVolume index=700 ← 约1秒后恢复
原因:
保护窗口之前只保护了 updateBtDeviceVolumeIndex(防止保存错误值)和 setDeviceVolume(防止持久化错误值),
但没有阻止蓝牙栈通过 onSetStreamVolume 直接修改 VolumeStreamState 的 index。
onSetStreamVolume 在独立的 Binder 线程执行,不需要 mDevicesLock,
直接调用 setStreamVolumeInt() 修改了内存中的音量值,导致瞬间生效。
解决: 在 onSetStreamVolume() 入口处增加保护窗口检查:
当设备是蓝牙设备、caller 是 com.android.bluetooth、且保护窗口激活时,直接 return。
这样蓝牙连接后 5 秒内,蓝牙栈的所有音量设置请求都会被拦截,彻底消除瞬间突变。
六、调试验证
手动在关键位置添加了包含 "see Debug Volume" 关键字的日志,打印具体音量值,方便过滤查看:
bash
adb logcat | grep "see Debug Volume"
日志覆盖的关键路径:
| 位置 | 日志内容 | 说明 |
|---|---|---|
AudioService.onSetStreamVolume() |
stream, index, device, caller | 任何流音量变化入口 |
AudioService.onSetStreamVolume() |
blocking BT stack volume change | 保护窗口拦截蓝牙栈 |
AudioService.setDeviceVolume() |
stream, device, index | 设备音量应用 |
AudioService 蓝牙保存触发 |
persist BT device volume | 蓝牙音量持久化触发 |
AudioService 同步扬声器 |
sync BT volume to speaker | 蓝牙音量同步到扬声器 |
AudioDeviceBroker.postSetAvrcpAbsoluteVolumeIndex() |
absVolIndex | AVRCP 绝对音量下发 |
AudioDeviceBroker.postSetLeAudioVolumeIndex() |
volIndex, maxIndex, stream | LE Audio 音量下发 |
AudioDeviceBroker.postSetHearingAidVolumeIndex() |
volIndex, stream | 助听器音量下发 |
AudioDeviceInventory A2DP 连接 |
restored savedVolume / sync from speaker | A2DP 连接时恢复/同步音量 |
AudioDeviceInventory LE Audio 连接 |
volIndex, savedVolume | LE Audio 连接时恢复音量 |
AudioDeviceInventory HearingAid 连接 |
volIndex, stream, address | 助听器连接时设置音量 |
AudioDeviceInventory.updateBtDeviceVolumeIndex() |
saving BT volIndex | 蓝牙设备音量保存 |
AudioDeviceInventory 保护窗口 |
skipping BT volume save | 保护窗口内跳过保存 |
正常工作时的日志示例:
# 音箱A连接,恢复保存的音量
see Debug Volume: A2DP restored savedVolume=500 dev=0x80 addr=48:F3:F3:ED:78:AD
# 蓝牙栈AVRCP上报被拦截
see Debug Volume: blocking BT stack volume change during protection window, stream=0 index=1000
# 保护窗口内,音量保存也被跳过
see Debug Volume: skipping BT volume save during protection window elapsed=1500ms
# 5秒后用户手动调节音量,正常保存和同步
see Debug Volume: saving BT volIndex=600 dev=0x80 addr=48:F3:F3:ED:78:AD
see Debug Volume: sync BT volume to speaker, index=600
七、其他
1、小结
Android 蓝牙连接后声音突变的根本原因是三个因素叠加:
音量按设备类型存储(不区分蓝牙地址)、AVRCP 绝对音量覆盖、多线程竞争导致瞬间突变。
解决方案利用已有的 AdiDeviceState 按设备地址持久化机制,新增音量字段,
实现每个蓝牙设备独立记忆音量。通过保护窗口机制(5秒),在 onSetStreamVolume 入口和
updateBtDeviceVolumeIndex 两个层面同时拦截蓝牙栈的音量覆盖,彻底消除瞬间突变和持久化污染。
同时实现了蓝牙音量与扬声器音量的双向同步:
- 蓝牙连接时无保存记录 → 同步扬声器音量到蓝牙
- 蓝牙音量变化时 → 同步到扬声器(
volume_music_speaker和volume_music_bt_a2dp保持一致)
2、修改文件清单
| 文件 | 修改内容 | 改动量 |
|---|---|---|
AdiDeviceState.java |
新增 mVolumeIndex 字段、getter/setter、序列化/反序列化、equals/hashCode/toString |
~20 行 |
AudioDeviceInventory.java |
保护窗口机制、updateBtDeviceVolumeIndex()、修改 A2DP/LE Audio 连接分支 |
~60 行 |
AudioDeviceBroker.java |
postPersistBtDeviceVolume()、isBtVolumeProtectionActive() |
~15 行 |
AudioService.java |
onSetStreamVolume() 入口拦截、setDeviceVolume() 保存+同步 |
~25 行 |
所有文件位于:frameworks/base/services/core/java/com/android/server/audio/
代码主要就这三个文件,但是实际逻辑过程需要自己消化理解。
其实是不建议修改的,修改后,在不同方案上也不好移植,Android不同版本的差异很大;
本文只是作为一个研究测试参考。