Android 不同的蓝牙音箱连接后声音突变问题分析解决

Android 不同的蓝牙音箱连接后声音突变问题分析解决

文章目录


一、前言

在 Android 设备上使用蓝牙音箱时,发现一个影响用户体验的问题:

连接蓝牙音箱 A 并调节到合适音量后,断开再连接蓝牙音箱 B,音箱 B 的音量会突变为最大值,声音非常大。

再次连接音箱 A 时,之前调节好的音量也丢失了。

多次切换蓝牙音响设备后,有概率声音会突变成最大值。

经过分析,问题由三个因素叠加导致:

  1. 音量按设备类型存储,不按设备地址存储 --- 所有 A2DP 蓝牙设备共享同一个音量值
  2. AVRCP 绝对音量覆盖 --- 蓝牙栈连接后上报设备自身音量(通常是最大值),覆盖系统侧音量
  3. 多线程竞争 --- AVRCP 音量上报在独立线程执行,不受设备连接锁保护,导致瞬间音量突变

本文记录完整的分析过程、多轮踩坑和最终解决方案。

涉及的代码基于 Android 15(AOSP),核心修改在 frameworks/base/services/core/java/com/android/server/audio/ 目录下。

其实这个问题是正常的,Android系统默认是启动蓝牙绝对音量,不同的蓝牙设备会有不同的音量大小;

比如:

系统本身音量40%,连接蓝牙A,变成80%,断开蓝牙系统音量显示40%,再连接蓝牙音量会显示80;

连接蓝牙后修改音量大小,音量值大小默认值保存到蓝牙音箱设备的,

这时候断开蓝牙设备,系统音量值还是会显示40%音量。

如果实在要适配成:

复制代码
比如 :设备音量为50%,不管切换连接多个蓝牙音量都是50%,或者断开蓝牙设备,查看音量都是50%;
后续:不管是在蓝牙连接还是不连接的情况下,调节音量大小后,连不连接蓝牙,音量大小都是记忆的;

可以参考下面的代码适配,但是也可能会导致一些音量大小问题。


二、问题现象

操作步骤:

  1. 连接蓝牙音箱 A,调节音量到 50%
  2. 断开音箱 A,连接蓝牙音箱 B
  3. 音箱 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 秒的音量突变窗口。

用户听到的"声音变最大"就是这个窗口期。


四、解决方案

整体思路:

  1. AdiDeviceState 中新增音量字段,实现按设备地址记忆音量
  2. 蓝牙连接时恢复保存的音量,无保存记录时同步扬声器音量
  3. 音量变化时保存到对应设备的 AdiDeviceState,并同步到扬声器
  4. 通过保护窗口机制,阻止蓝牙栈 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");
    }

关键点:mLastBtConnectTimeMsif/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 被调用),

此时 VolumeStreamStateDEVICE_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_speakervolume_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不同版本的差异很大;

本文只是作为一个研究测试参考。

相关推荐
JJay.6 小时前
Android BLE 里,MTU、分包和长数据发送到底该怎么处理
android
Willliam_william6 小时前
QEMU学习之路(12)— 使用qemu-system-riscv64测试IOMMU
大数据·学习·elasticsearch
2zcode6 小时前
基于Chaboche物理约束与LSTM残差学习的316L不锈钢循环塑性灰箱本构建模研究
学习·机器学习·lstm
my_daling6 小时前
DSMC通信协议理解,以及如何在FPGA上实现DSMC从设备(2)
学习·fpga开发
70asunflower6 小时前
半导体产业的经济逻辑、技术瓶颈与AI芯片格局:一份学习笔记
人工智能·笔记·学习
2501_915909067 小时前
iOS应用签名的三种方法全解析:从官方到第三方工具
android·ios·小程序·https·uni-app·iphone·webview
凉、介7 小时前
C 语言类型强转引发的隐蔽内存破坏问题分析
c语言·开发语言·笔记·学习·嵌入式
知识分享小能手7 小时前
R语言入门学习教程,从入门到精通,R语言分布式数据可视化(6)
学习·信息可视化·r语言
Robot_Nav7 小时前
Mobile ALOHA:通过低成本全身远程操作 to 实现双手机器人移动操控学习【文献解读】
学习·机器人·模仿学习·双臂移动机器人