CallAudioManager 是如何工作的
CallAudioManager是干啥的呢?单词分来来写 Call Audio Manager,一个管理通话中音频状态的类。
初始化
一张图看清CallAudioManager怎么来的 。
在TeleService创建的时候对TelecomGlobals进行初始化,然后new出一个CallsManager,在CallsManager.java的构造函数中new出一个CallAudioManager(),带三个参数CallAudioManager(context, statusBarNotifier, mWiredHeadsetManager)。
文件结构
然后看一下CallAudioManager的继承和实现关系
CallAudioManager extends CallsManagerListenerBase
implements WiredHeadsetManager.Listener {
CallAudioManager.java继承CallsManagerListenerBase.java(其实就是CallsManager的CallsManagerListener接口),实现WiredHeadsetManager.Listener(),
因此它重写了CallsManagerListenerBase中的一系列方法,实现了WiredHeadsetManager的接口onWiredHeadsetPluggedInChanged(),见下图:
从这张图里面我们可以大致了解到CallAudioManager关心的一些事情,在Call发生变化的时候,在
有线耳机插入/拔出的时候,这个类会做一些操作设置,保存或者更新Audio的一些信息。看到onWiredHeadsetPluggedInChanged()这个方法,可能有些人就想到 了,既然有一个方法关心有线耳机的插入/拔出状态的变化,那怎么没有一个方法处理蓝牙耳机连接/断开的状态呢?实际上是有的,只不过它不是像有线耳机那样是通过实现接口实现的,而是在内部写了几个方法监听处理蓝牙的状态,其中一个与有线耳机插入/拔出对应的方法就是onBluetoothStateChange()。
通过下面这段CallAudioManager的构造函数可以看到,在CallAudioManager初始话的时候new了一个BluetoothManager(),
CallAudioManager(Context context, StatusBarNotifier statusBarNotifier,
WiredHeadsetManager wiredHeadsetManager) {
mStatusBarNotifier = statusBarNotifier;
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mBluetoothManager = new BluetoothManager(context, this);
mWiredHeadsetManager = wiredHeadsetManager;
mWiredHeadsetManager.addListener(this);
saveAudioState(getInitialAudioState(null));
mAudioFocusStreamType = STREAM_NONE;
mContext = context;
}
而WiredHeadsetManager()却是在CallsManager初始化的时候跟CallAudioManager一起被new出来的,感觉WiredHeadset和Bluetooth两者不平级一样,暂时不知道是出于什么考虑。
/**
* Initializes the required Telecom components.
*/
CallsManager(Context context, MissedCallNotifier missedCallNotifier,
BlacklistCallNotifier blacklistCallNotifier,
PhoneAccountRegistrar phoneAccountRegistrar) {
...
mWiredHeadsetManager = new WiredHeadsetManager(context);
mCallAudioManager = new CallAudioManager(context, statusBarNotifier, mWiredHeadsetManager);
...
}
这是一个管理通话中音频状态的类,那么它必然要设置新的状态,并把状态的变化通知出去。由此因此这个类中可能是最重要的一个方法setSystemAudioState(),
private void setSystemAudioState(
boolean force, boolean isMuted, int route, int supportedRouteMask) {
if (!hasFocus()) {
return;
}
AudioState oldAudioState = mAudioState;
saveAudioState(new AudioState(isMuted, route, supportedRouteMask));
if (!force && Objects.equals(oldAudioState, mAudioState)) {
return;
}
Log.i(this, "changing audio state from %s to %s", oldAudioState, mAudioState);
// Mute.
if (mAudioState.isMuted() != mAudioManager.isMicrophoneMute()) {
Log.i(this, "changing microphone mute state to: %b", mAudioState.isMuted());
mAudioManager.setMicrophoneMute(mAudioState.isMuted());
}
// Audio route.
if (mAudioState.getRoute() == AudioState.ROUTE_BLUETOOTH) {//设为蓝牙
turnOnSpeaker(false);
turnOnBluetooth(true);
} else if (mAudioState.getRoute() == AudioState.ROUTE_SPEAKER) {//扬声器
turnOnBluetooth(false);
turnOnSpeaker(true);
} else if (mAudioState.getRoute() == AudioState.ROUTE_EARPIECE ||
mAudioState.getRoute() == AudioState.ROUTE_WIRED_HEADSET) {//听筒或者有线耳机
turnOnBluetooth(false);
turnOnSpeaker(false);
}
if (!oldAudioState.equals(mAudioState)) {
CallsManager.getInstance().onAudioStateChanged(oldAudioState, mAudioState);
updateAudioForForegroundCall();
}
}
前面所有执行的步骤到最后几乎都是为了执行到这个方法里,去打开/关闭Speaker,“打开/关闭”蓝牙,然后把状态的变化通知出去。这里并没有单独写一个方法通知状态的变化,只是在这个方法内调用了CallsManager.getInstance().onAudioStateChanged(oldAudioState, mAudioState);
。
值得注意的是turnOnSpeaker(true)是通过mAudioManager.setSpeakerphoneOn(on);的方式,而turnOnBluetooth(true);则是通过mBluetoothManager.connectBluetoothAudio();的方式,蓝牙不归AudioManager管?想想也对,有可能是连接的蓝牙键盘呢。
另外有一点,切换到听筒和有线耳机的方法居然后把扬声器和蓝牙都关掉。
CallAudio更新
起先我是想写“audio更新”的,但是为什么改成“CallAudio更新”呢?因为这个audio跟call的关系太密切了,可以说call不存在audio就不存在,而且Android M上很多跟call有关的audio相关的类,都由Audio更名为CallAudio了(如AudioState.java -> CallAudioState.java)。 前面说到这个类里面最重要的方法是setSystemAudioState(),那么看一下调用层级:
然后画成示意图的形式:
结合代码对上图解读一下。(圆形的setSystemAudioState()和右侧的圆角长方形方法同名,携带参数不一样) 左侧的圆角长方形表示“设置为默认的audio state”调用这个方法的时候,不用携带audio相关的参数,方法内会自己生成一个初始化的audioState。两个椭圆代表的场景是“所有通话被移除,恢复默认值”,和“满足‘从不是VoiceCall到VoiceCall’的时候设置一个初始化的audioState”。 右侧圆角长方形表示“设置audio state”,调用这个方法带4个参数,分别是:强制设置,是否mute,audioState,当前所有支持的audioState。 最右侧6个椭圆中只有最下面的setAudioRoute()是手动设置,我们在InCallUI界面上操作都是从这个入口传进来的。其余可以认为是自动设置。
AudioState
下面我们再说一下最关键的一个变量mAudioState。 mAudioState这个变量几乎携带了所有的audio相关的信息,关键的两个:
/**
* @return The current audio route being used. //当前audio传输使用的方式
*/
public int getRoute() {
return route;
}
/**
* @return Bit mask of all routes supported by this call. //当前所有支持的传输方式
*/
public int getSupportedRouteMask() {
return supportedRouteMask;
}
在上面我们说过,audioState发生变化时是通过CallsManager.getInstance().onAudioStateChanged(oldAudioState, mAudioState);
这行代码更新的,可以理解为把mAudioState的值广播出去。mAudioState可以理解为数据的源头,一旦mAudioState除了问题,那么上层肯定会出现问题。
(铃声存在时间超出call的生命周期)
但是现有代码中,在没有没有call和没有铃声的时候,这个值是不会更新的(注①),所以这将会导致一个问题,什么问题呢?读者们不妨想一下。
问题是,在call和铃声不存在时,插拔耳机,连接/断开蓝牙耳机导致的audio变化不会更新到mAudioState中,进而上层也不会知晓audioState状态的变化,也就导致audioButton显示错误。这个现象只在来电状态可以看到,来电被接听后audioButton显示正常,因为在onIncomingCallAnswered()里会setSystemAudioState()然后更新audioState。
思考一下解法?(勿扰模式,多路通话)
04.14补充 我们发现在Android M 版本上,先建立一路通话,打开Speaker,然后新增一路通话,会使打开着的Speaker关闭。 查找提交发现,commit信息中写到
Route audio to earpiece in DIALING state if call is a Voice call and bluetooth or wired headset is not connected. This fixes an issue where if we make a first video call and second outgoing call is voice, audio is routed to speaker which is not expected.
在之前的audio相关的博客中我介绍过在视频电话中Speaker的一些使用策略,上面commit message中提到的也是“第一通是视频电话,第二通是语音电话”的场景,但是我们发现实际使用的时候,两个都是语音电话的情况下也会关闭。 其实这里到不存在对错,只是可能需要重新考虑一下audio切换的策略。还有需要看一下上面那条提交是否准确实现了他想要的效果。