背景
Android 12 上发现存在蓝牙外放问题,原因是存在多个应用设置通话音量,在建立SCO连接时,如果本应用不是通话音量的mode owner,则系统会拒绝为该应用建立sco。
也就是只有mode owner对应的应用才可以建立sco。
本篇从系统机制和dumpsys 角度分析根本原因
原因分析
首先提一下,sco连接失败的代码表现是执行startBluetoothSco后,收不到任何蓝牙状态的广播,就如同完全没执行过一样。
那分析就从该函数开始
代码语言:javascript复制 public void startBluetoothSco(){
final IAudioService service = getService();
try {
service.startBluetoothSco(mICallBack,
getContext().getApplicationInfo().targetSdkVersion);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
继续看AudioService
代码语言:javascript复制 /** @see AudioManager#startBluetoothSco() */
public void startBluetoothSco(IBinder cb, int targetSdkVersion) {
if (!checkAudioSettingsPermission("startBluetoothSco()")) {
return;
}
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final int scoAudioMode =
(targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR2) ?
BtHelper.SCO_MODE_VIRTUAL_CALL : BtHelper.SCO_MODE_UNDEFINED;
final String eventSource = new StringBuilder("startBluetoothSco()")
.append(") from u/pid:").append(uid).append("/")
.append(pid).toString();
new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
.setUid(uid)
.setPid(pid)
.set(MediaMetrics.Property.EVENT, "startBluetoothSco")
.set(MediaMetrics.Property.SCO_AUDIO_MODE,
BtHelper.scoAudioModeToString(scoAudioMode))
.record();
startBluetoothScoInt(cb, pid, scoAudioMode, eventSource);
}
这儿会走到startBluetoothScoInt里,没其他真正逻辑:
代码语言:javascript复制 void startBluetoothScoInt(IBinder cb, int pid, int scoAudioMode, @NonNull String eventSource) {
MediaMetrics.Item mmi = new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
.set(MediaMetrics.Property.EVENT, "startBluetoothScoInt")
.set(MediaMetrics.Property.SCO_AUDIO_MODE,
BtHelper.scoAudioModeToString(scoAudioMode));
if (!checkAudioSettingsPermission("startBluetoothSco()") ||
!mSystemReady) {
mmi.set(MediaMetrics.Property.EARLY_RETURN, "permission or systemReady").record();
return;
}
final long ident = Binder.clearCallingIdentity();
mDeviceBroker.startBluetoothScoForClient(cb, pid, scoAudioMode, eventSource);
Binder.restoreCallingIdentity(ident);
mmi.record();
}
这儿调用就会走到AudioDeviceBroker里
代码语言:javascript复制 /*package*/ void startBluetoothScoForClient(IBinder cb, int pid, int scoAudioMode,
@NonNull String eventSource) {
if (AudioService.DEBUG_COMM_RTE) {
Log.v(TAG, "startBluetoothScoForClient_Sync, pid: " pid);
}
synchronized (mSetModeLock) {
synchronized (mDeviceStateLock) {
AudioDeviceAttributes device =
new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, "");
postSetCommunicationRouteForClient(new CommunicationClientInfo(
cb, pid, device, scoAudioMode, eventSource));
}
}
}
这儿会抛到Looper里,对应的实现如下:
代码语言:javascript复制@GuardedBy("mDeviceStateLock")
/*package*/ void setCommunicationRouteForClient(
IBinder cb, int pid, AudioDeviceAttributes device,
int scoAudioMode, String eventSource) {
if (AudioService.DEBUG_COMM_RTE) {
Log.v(TAG, "setCommunicationRouteForClient: device: " device);
}
AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
"setCommunicationRouteForClient for pid: " pid
" device: " device
" from API: " eventSource)).printLog(TAG));
final boolean wasBtScoRequested = isBluetoothScoRequested();
CommunicationRouteClient client;
// Save previous client route in case of failure to start BT SCO audio
AudioDeviceAttributes prevClientDevice = null;
client = getCommunicationRouteClientForPid(pid);
if (client != null) {
prevClientDevice = client.getDevice();
}
if (device != null) {
client = addCommunicationRouteClient(cb, pid, device);
if (client == null) {
Log.w(TAG, "setCommunicationRouteForClient: could not add client for pid: "
pid " and device: " device);
}
} else {
client = removeCommunicationRouteClient(cb, true);
}
if (client == null) {
return;
}
boolean isBtScoRequested = isBluetoothScoRequested();
if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) {
if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) {
Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for pid: "
pid);
// clean up or restore previous client selection
if (prevClientDevice != null) {
addCommunicationRouteClient(cb, pid, prevClientDevice);
} else {
removeCommunicationRouteClient(cb, true);
}
postBroadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
}
} else if (!isBtScoRequested && wasBtScoRequested) {
mBtHelper.stopBluetoothSco(eventSource);
}
sendLMsgNoDelay(MSG_L_UPDATE_COMMUNICATION_ROUTE, SENDMSG_QUEUE, eventSource);
}
这儿的逻辑简单看就是:
先查下是否已经有sco请求了 如果以前没有sco请求,现在要请求了,那就开始请求sco 这样理解上看没毛病,具体看看就会发现还有一些细节,比如isBluetoothScoRequested,是如何判断是否有sco请求的呢?
代码语言:javascript复制 /*package*/ boolean isBluetoothScoRequested() {
return isDeviceRequestedForCommunication(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
}
private boolean isDeviceRequestedForCommunication(int deviceType) {
synchronized (mDeviceStateLock) {
AudioDeviceAttributes device = requestedCommunicationDevice();
return device != null && device.getType() == deviceType;
}
}
@GuardedBy("mDeviceStateLock")
private AudioDeviceAttributes requestedCommunicationDevice() {
CommunicationRouteClient crc = topCommunicationRouteClient();
AudioDeviceAttributes device = crc != null ? crc.getDevice() : null;
if (AudioService.DEBUG_COMM_RTE) {
Log.v(TAG, "requestedCommunicationDevice, device: "
device "mAudioModeOwner: " mAudioModeOwner.toString());
}
return device;
}
// 注意了!!!!!!!!!,这儿是最最关键的地方
@GuardedBy("mDeviceStateLock")
private CommunicationRouteClient topCommunicationRouteClient() {
for (CommunicationRouteClient crc : mCommunicationRouteClients) {
if (crc.getPid() == mAudioModeOwner.mPid) {
return crc;
}
}
if (!mCommunicationRouteClients.isEmpty() && mAudioModeOwner.mPid == 0) {
return mCommunicationRouteClients.get(0);
}
return null;
}
判断routeclient 的标准就是是否是modeowner,或者当前还没有modeownner,如果通话音量被其他应用设置了后,那明显不会是modeownner,因此这儿就会返回null。再倒推回去,isBluetoothScoRequested的返回值就是false。
那接下来获取client:getCommunicationRouteClientForPid,如果是首次设置,那就是null,接下来就是通过addCommunicationRouteClient进行add,这儿就是往列表中添加,那一定会添加成功。好的,那接下来就开始判断本次是否是启动sco了:isBluetoothScoRequested
按照刚才的逻辑,由于不是modeowner,那返回必然是false。也就是本次也不认为是sco请求。
如果本地和之前都没有sco请求,那就不操作sco了,直接更新路由:
代码语言:javascript复制private void onUpdateCommunicationRoute(String eventSource) {
AudioDeviceAttributes preferredCommunicationDevice = preferredCommunicationDevice();
//preferredCommunicationDevice 会返回null
if (AudioService.DEBUG_COMM_RTE) {
Log.v(TAG, "onUpdateCommunicationRoute, preferredCommunicationDevice: "
preferredCommunicationDevice " eventSource: " eventSource);
}
AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
"onUpdateCommunicationRoute, preferredCommunicationDevice: "
preferredCommunicationDevice " eventSource: " eventSource)));
if (preferredCommunicationDevice == null
|| preferredCommunicationDevice.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
// 返回是null,所以即使是打开sco,到了这儿也变成了关闭sco了
AudioSystem.setParameters("BT_SCO=off");
} else {
AudioSystem.setParameters("BT_SCO=on");
}
if (preferredCommunicationDevice == null) {
AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice();
if (defaultDevice != null) {
setPreferredDevicesForStrategySync(
mCommunicationStrategyId, Arrays.asList(defaultDevice));
setPreferredDevicesForStrategySync(
mAccessibilityStrategyId, Arrays.asList(defaultDevice));
} else {
removePreferredDevicesForStrategySync(mCommunicationStrategyId);
removePreferredDevicesForStrategySync(mAccessibilityStrategyId);
}
} else {
setPreferredDevicesForStrategySync(
mCommunicationStrategyId, Arrays.asList(preferredCommunicationDevice));
setPreferredDevicesForStrategySync(
mAccessibilityStrategyId, Arrays.asList(preferredCommunicationDevice));
}
onUpdatePhoneStrategyDevice(preferredCommunicationDevice);
}
接下来再获取preferedcommunicationdevice:
代码语言:javascript复制 @Nullable private AudioDeviceAttributes preferredCommunicationDevice() {
boolean btSCoOn = mBluetoothScoOn && mBtHelper.isBluetoothScoOn();
if (btSCoOn) {
// Use the SCO device known to BtHelper so that it matches exactly
// what has been communicated to audio policy manager. The device
// returned by requestedCommunicationDevice() can be a dummy SCO device if legacy
// APIs are used to start SCO audio.
AudioDeviceAttributes device = mBtHelper.getHeadsetAudioDevice();
if (device != null) {
return device;
}
}
AudioDeviceAttributes device = requestedCommunicationDevice();
if (device == null || device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
// Do not indicate BT SCO selection if SCO is requested but SCO is not ON
return null;
}
return device;
}
如果调用了setBluetoothScoOn,那mBluetoothScoOn 可以是true,可是isBluetoothScoOn就会是false,因为sco 还没打开。
那这儿就会还是返回null。
返回null后,就会不管是不是开启sco,统一执行关闭sco:AudioSystem.setParameters("BT_SCO=off")
到了这儿总结下,如果不是modeowner,那么不管开不开sco,都会变成关闭sco。那接下来用log论证下:
代码语言:javascript复制dumpsys media.metrics:
1951: {audio.bluetooth, (10-10 16:47:45.360), (10097, 25565, 10097), (event#=startBluetoothSco, scoAudioMode=SCO_MODE_UNDEFINED)}
1952: {audio.bluetooth, (10-10 16:47:45.361), (android.uid.system, 0, 1000), (event#=startBluetoothScoInt, scoAudioMode=SCO_MODE_UNDEFINED)}
dumpsys audio
10-10 16:47:45:360 onUpdateCommunicationRoute, preferredCommunicationDevice: null eventSource: startBluetoothSco()) from u/pid:10097/25565
10-10 16:47:45:362 removePreferredDevicesForStrategySync, strategy: 14
10-10 16:47:45:362 removePreferredDevicesForStrategySync, strategy: 17
dumpsys media.audio_flinger
10-10 16:47:13.696 UID 1000, 1 KVP received: BT_SCO=off
10-10 16:47:26.100 UID 1000, 1 KVP received: A2dpSuspended=false
10-10 16:47:33.188 UID 1000, 1 KVP received: BT_SCO=off
10-10 16:47:45.110 UID 1000, 1 KVP received: BT_SCO=off
10-10 16:47:45.361 UID 1000, 1 KVP received: BT_SCO=off
看到 虽然执行的是startBluetoothSco, 却一直在发送关闭sco命令。代码和log 对的上了。