背景
Android 12 上发现存在蓝牙外放问题,原因是存在多个应用设置通话音量,在建立SCO连接时,如果本应用不是通话音量的mode owner,则系统会拒绝为该应用建立sco。
也就是只有mode owner对应的应用才可以建立sco。
本篇从系统机制和dumpsys 角度分析根本原因
原因分析
首先提一下,sco连接失败的代码表现是执行startBluetoothSco后,收不到任何蓝牙状态的广播,就如同完全没执行过一样。
那分析就从该函数开始
public void startBluetoothSco(){
final IAudioService service = getService();
try {
service.startBluetoothSco(mICallBack,
getContext().getApplicationInfo().targetSdkVersion);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
继续看AudioService
/** @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里,没其他真正逻辑:
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里
/*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里,对应的实现如下:
@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请求的呢?
/*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了,直接更新路由:
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:
@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论证下:
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 对的上了。
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/21357,转载请注明出处。
评论0