最终还是要回到AudioService中来,它才是音频相关操作的主基地。
1. 处理来自WiredAccessoryObserver的通知
AudioService会如何处理外设的可用状态变化呢?仔细想想,在开发播放器的时候一定接触过ACTION_AUDIO_BECOMING_NOISY和ACTION_HEADSET_PLUG这两个广播吧。另外,更重要的是,这些变化需要让底层的AudioPolicy知道。所以,笔者认为AudioService外设状态管理分为三个内容:
管理发送ACTION_AUDIO_BECOMING_NOISY广播。
发送设备状态变化的广播,通知应用。
将其变化通知底层。
从WiredHeadsetObserver调用的setWiredDeviceConnectionState()函数开始:
[AudioService.java-->AudioService.setWiredDeviceConnectionState()]
public void setWiredDeviceConnectionState(int device, int state, String name) {
synchronized (mConnectedDevices) {
// 发送ACTION_AUDIO_BECOMING_NOISY广播的地方
int delay = checkSendBecomingNoisyIntent(device, state);
// 又是发送消息给mAudioHandler,注意这个消息有可能是延时的
// 这取决于checkSendBecomingNoisyIntent的返回值:delay
queueMsgUnderWakeLock(mAudioHandler,
MSG_SET_WIRED_DEVICE_CONNECTION_STATE,
device,
state,
name,
delay);
}
}
此函数负责两项工作:调用checkSendBecomingNoisyIntent()函数及发送SET_WIRED_DEVICE_CONNECTION_STATE消息给mAudioHandler。
checkSendBecomingNoisyIntent()函数的目的是判断当前状态的变化是否有必要发送BECOMING_NOISY广播。这个广播用于警告所有媒体播放应用声音即将从手机外放中进行播放。在绝大部分情况下,收到这个广播的应用都应当立即暂停播放,以避免用户无意识地泄露自己的隐私或打扰到周围的其他人。另外,这个函数的返回值决定了SET_WIRED_DEVICE_CONNECTION_STATE消息是否需要延时处理。其代码如下:
[AudioService.java-->AudioService.checkSendBecomingNoisyIntent()]
private int checkSendBecomingNoisyIntent(int device, int state) {
int delay = 0;
// 发送BECOMING_NOISY广播的前两个条件如下:
// 1.外设被拔除
// 2.外设是mBecomingNoisyIntentDevices指定的外设之一
// 既然这些设备从手机拔除后会AUDIO_BECOMING_NOISY,不妨称它们为安静外设
if ((state == 0) && ((device & mBecomingNoisyIntentDevices) != 0)) {
int devices = 0;
// 收集所有连接在手机上的安静外设
for (int dev : mConnectedDevices.keySet()) {
if ((dev & mBecomingNoisyIntentDevices) != 0) {
devices |= dev;
}
}
// 发送 BECOMING_NOISY广播的第三个条件:移除的设备必须是连接在手机上的最后一个安静外设
// 同时也是推迟后续处理的第一个条件:发送了BECOMING_NOISY广播
if (devices == device) {
delay = 1000; // 确定后续对这个状态变化的处理向后推迟1秒
sendBecomingNoisyIntent(); // 发送BECOMING_NOISY广播
}
}
// 推迟后续处理的另外一个条件:如果有和外设连接状态相关的延迟消息尚未被处理,那么
// 也必须推后消息的处理
if (mAudioHandler.hasMessages(MSG_SET_A2DP_CONNECTION_STATE) ||
mAudioHandler.hasMessages(MSG_SET_WIRED_DEVICE_CONNECTION_STATE)) {
delay = 1000;
}
return delay;
}
代码不长,有价值的内容不少。BECOMING_NOISY广播发出的条件是最后一个安静外设被拔出,这个很好理解。而推迟MSG_SET_WIRED_DEVICE_CONNECTION_STATE消息的生效时间这种做法可能一时难以弄明白。不过暂时先不管它,等我们了解了外设连接状态变化的流程后再解释它的意义。
回到setWiredDeviceConnectionState (),调用checkSendBecomingNoisyIntent()函数后,它发送MSG_SET_WIRED_DEVICE_CONNECTION_STATE给mAudioHandler,此消息生效后,mAudioHandler调用onSetWiredDeviceConnectionState函数。
[AudioService.java-->AudioHandler.onSetWiredDeviceConnectionState()]
private void onSetWiredDeviceConnectionState(int device, int state, String name)
{
synchronized (mConnectedDevices) {
// 如果拔下普通耳机,则会强制要求使用蓝牙耳机作为输出设备
if ((state == 0) && ((device == AudioSystem.DEVICE_OUT_WIRED_HEADSET) ||
(device == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE))) {
setBluetoothA2dpOnInt(true);
}
// 这个函数对AudioPolicy进行了通知
handleDeviceConnection((state == 1), device, "");
// 如果插入普通耳机,则会取消强制使用蓝牙耳机的设置
if ((state != 0) && ((device == AudioSystem.DEVICE_OUT_WIRED_HEADSET) ||
(device == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE))) {
setBluetoothA2dpOnInt(false);
}
// 从名字就可以看出,这是用来广播外设状态变化通知的,将通知对此感兴趣的应用程序
sendDeviceConnectionIntent(device, state, name);
}
}
在这个函数中,我们需要重点关注的是对handleDeviceConnection()和sendDevice-ConnectionIntent两个函数的调用。它们分别用来通知AudioPolicy与上层应用。
另外,还可以看到,在handleDeviceConnection()函数上下有一对关于蓝牙耳机的操作。从其实现上可以看出,如果拔出普通耳机,系统将会强制使用蓝牙耳机进行输出。如果插入耳机则会取消这个设置。这种操作完全可以放在AudioPolicyManager中实现。
看一下通知AudioPolicy的handleDeviceConnection()函数的实现吧!
[AudioService.java-->AudioService.handleDeviceConnection()]
private boolean handleDeviceConnection(boolean connected, int device, String params){
synchronized (mConnectedDevices) {
boolean isConnected = (mConnectedDevices.containsKey(device) &&
(params.isEmpty() || mConnectedDevices.get(device).equals(params)));
if (isConnected && !connected) {
// 外设被拔出,通过AudioSystem将状态设置到底层的AudioPolicyService
AudioSystem.setDeviceConnectionState(device,
AudioSystem.DEVICE_STATE_UNAVAILABLE,
mConnectedDevices.get(device));
mConnectedDevices.remove(device);
return true;
} else if (!isConnected && connected) {
// 外设被插入
AudioSystem.setDeviceConnectionState(device,
AudioSystem.DEVICE_STATE_AVAILABLE,
params);
mConnectedDevices.put(new Integer(device), params);
return true;
}
}
return false;
}
很简单吧?如果读者对卷I第7章的内容比较熟悉,那么一定知道AudioSystem.setDeviceConnectionState()这个函数意味着什么。它将更新底层的AudioPolicy中缓存的可用设备列表,同时,如果正在进行音频播放,那么这个函数还将触发音频设备的重新选择。
这一节提到“可用设备列表”的次数很多,很多地方都使用了这个概念。归纳一下,在本节所讨论的内容里,有三个地方有可用设备列表:
1)WiredAccessoryObserver: 目的是确认外设的状态变化是否合法,是否需要报告给AudioService。
2)AudioService: 它以一个Hashtable的形式保存了一个可用设备列表,它为AudioService向应用及底层AudioPolicyManager发送通知提供依据。
3)AudioPolicyManager: 它保存的可用设备列表在AudioPolicyManager需要重新选择音频输出设备时提供候选。
2. 关于推迟处理外设状态
前面讨论checkSendBecomingNoisyIntent()函数的实现时提到了根据某些条件,有可能使MSG_SET_WIRED_DEVICE_CONNECTION_STAT延迟生效1秒。在这种情况下应用会在1秒之后才能收到设备状态变化的广播,同时,AudioPolicy也要在1秒之后才能更新可用设备列表并进行必要的设备切换。为什么要这么做呢?想想推迟的条件:
最后一个安静外设被移除,发送了BECOMING_NOISY广播。
队列中尚有两个消息在等候处理:MSG_SET_WIRED_DEVICE_CONNECTION_STATE 和MSG_SET_A2DP_CONNECTION_STATE。
只要这两个条件有一个满足,就会发生1秒推迟。下面分别讨论。
关于第一个条件,当最后一个安静外设被移除后,手机上可用的音频输出设备就只剩下扬声器了(听筒不能算是常规的音频输出设备,它只有在通话过程中才会用到)。那么在MSG_SET_WIRED_DEVICE_CONNECTION_STAT生效后,AudioPolicyManager将会切换输出到扬声器,此时正在播放的音频就会被外放出来。
很多时候,这并不是用户所期望的,用户可能不希望他人知道自己在听什么,或者不希望在某些场合下扬声器发出的声音打扰到其他人。何况耳机被拔除有可能还是个意外。所以,正在进行音频播放的应用可能希望收到耳机等安静设备被拔出时的通知,并且在收到后暂停播放。
读者可能会有疑问,在sendDeviceConnectionIntent()中不是发送了状态通知的广播了吗?其实,这个状态通知广播用在其他情况下可以,但是用在上述情况中是有问题的。按照上面的讨论,执行sendDeviceConnectionIntent()之前,先执行了handleDeviceConnection(),它会更新底层的可用设备列表,并且触发设备切换。于是应用有可能在收到状态通知之前,输出设备已经被切换成扬声器了,直到应用收到通知后暂停回放,这段时间内就会发生扬声器的漏音。
所以,Android引入了一个新的广播来应对这个问题,那就是BECOMING_NOISY广播。这个广播只有在最后一个安静外设被移除后才会发出,于是应用可以精确地知道音频即将从扬声器进行播放,而且后续的设备切换等动作被推迟了1秒,应用就有充足的时间收到BECOMING_NOISY广播并暂停播放。在正常情况下,这种做法可以杜绝漏音的情况出现。这是第一个延时条件的意义。
至于第二个条件,队列中尚有以下两个消息等候处理:
MSG_SET_WIRED_DEVICE_CONNECTION_STATE 和MSG_SET_A2DP_CONNECTION_STATE,这其实是不得已的一种做法。考虑一下,为什么队列中尚有这两个消息在等候处理呢?一个是mAudioHandler所在的线程发生了阻塞,另一个就是这两个消息被延迟发送了。根据Handler现有的接口没有办法得知是哪一种情况,但是在正常情况下都是第二种,也是比较麻烦的一种情况。因为在这种情况下,如果正常发送MSG_SET_WIRED_DEVICE_CONNECTION_STATE消息,那么它的生效时间将会早于正在队列中排队的那两个消息。如此一来,就会发生外设可用状态紊乱的问题。所以,AudioService迫不得已在这种情况下推迟发送1秒。读者可以做个试验,快速地在手机上拔插耳机,将会看到通知栏内的耳机图标的变化总是会延迟1秒。
我们在之前的分析中没有见过MSG_SET_A2DP_CONNECTION_STATE,它和讨论的MSG_SET_WIRED_DEVICE_CONNECTION_STATE意义是一样的,而且有着几乎相同的处理逻辑,不过它是与蓝牙耳机相关的。