分享一下这几天改蓝牙通话无声的bug,文章有点长,希望各位看官看完能帮助到大家—今天分析的主题是蓝牙通话没有声音之运行流程分析
蓝牙通话分别有七个阶段,基本上每个阶段都会走到底层,把数据回调到上层,这么一个流程叫一个阶段;回调的数据代表一个状态,而我这个问题就在于底层回调数据到上层时,携带的数据不满足上层的要求 导致无法进入下一个阶段而产生的蓝牙通话无声问题;
蓝牙通话的七个连接阶段使用的模式是:状态机模式,没弄明白状态机模式之前有点不好入手;我这里分享一下状态机模式的详解文章:状态机模式介绍
vendor/mediatek/proprietary/packages/apps/Bluetooth/src/com/android/bluetooth/hfp/HeadsetNativeInterface.java
vendor/mediatek/proprietary/packages/apps/Bluetooth/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
vendor/mediatek/proprietary/packages/apps/Bluetooth/src/com/android/bluetooth/hfp/HeadsetService.java
frameworks/base/services/core/java/com/android/server/audio/BtHelper.java
蓝牙服务类别:
HeadsetPro-file(HSP)代表耳机功能,提供手机与耳机之间通信所需的基本功能。
HandProfile(HFP)则代表免提功能,HFP在HSP的基础上增加了某些扩展功能。
Advanced Audio Distribution Profile(A2DP),指的是 蓝牙音频传输模型协定。
蓝牙物理链路SCO(Synchronous Connection Oriented)主要用来传输对时间要求很高的数据通信,通常用于通话
蓝牙连接步骤分为七个状态,分别是:
Disconnected
该状态为初始化阶段,在蓝牙打开阶段会执行
Connecting
该状态在连接蓝牙时执行
Connected
该状态在蓝牙连接完成后执行
AudioConnecting
该状态在接听电话后执行,处于一个连接中的一个状态,很快就能连接上
AudioOn
该状态在接听电话后,AudioConnecting阶段完成后执行,这个时候通常就能用蓝牙耳机通话了
AudioDiconnecting
该状态在挂断电话后执行,此时又回到了Connected状态,因为蓝牙还是开启的
Disconnecting
该状态在蓝牙断开中执行,执行完成后回到Disconnected该状态
以上就是蓝牙连接到蓝牙断开的七个连接状态,每个状态都继承了状态机,接下来看看每个连接状态的具体方法,具体实现稍后分析
每个连接状态都有一个状态模式,都分别继承于HeadsetStateBase
和ConnectedBase
,它们共同的父父类就是State
,先来看一段代码,七个连接状态
// State machine states
private final Disconnected mDisconnected = new Disconnected();
private final Connecting mConnecting = new Connecting();
private final Disconnecting mDisconnecting = new Disconnecting();
private final Connected mConnected = new Connected();
private final AudioConnecting mAudioConnecting = new AudioConnecting();
private final AudioOn mAudioOn = new AudioOn();
private final AudioDisconnecting mAudioDisconnecting = new AudioDisconnecting();
...
// Initialize state machine
addState(mDisconnected);
addState(mConnecting);
addState(mDisconnecting);
addState(mConnected);
addState(mAudioOn);
addState(mAudioConnecting);
addState(mAudioDisconnecting);
setInitialState(mDisconnected);
分别把七个连接阶段加入到状态机中,初始化mDisconnected
为默认状态,每个状态机都有四个方法,具体看代码
class 七个连接状态 extends ConnectedBase 或者 HeadsetStateBase {
@Override
int getAudioStateInt() {
...
}
@Override
public void enter() {
...
}
// ...
@Override
public boolean processMessage(Message message) {
...
}
@Override
public void exit() {
...
}
}
这三个方法是每个连接阶段必有的,getAudioStateInt
用于一个常量代表当前返回的状态,enter
进入该方法,processMessage
事件逻辑处理,exit
退出当前状态,这四个方法是顺序执行。
之前说道基本上每个连接状态都会走到底层拿数据回调上层,这里我们来具体看看,主要分析AudioConnecting
阶段,大致分析其他阶段
class Disconnected extends HeadsetStateBase {
...
@Override
public boolean processMessage(Message message) {
...
if (!mNativeInterface.connectHfp(device)) { // 第一步
stateLogE("CONNECT failed for connectHfp(" + device + ")");
// No state transition is involved, fire broadcast immediately
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
break;
}
transitionTo(mConnecting); // 第二步
...
return HANDLED;
}
...
}
很关键的两步,第一步连接之前提到过的HFP服务,调用底层,连接成功后发送广播;第二步状态切换,进入下一步连接状态;这里的返回值一定要是true才行,才能进入exit,HANDLED
默认就是true;底层回调:HeadsetNativeInterface.java # private native boolean connectHfpNative(byte[] address);
class Connecting extends HeadsetStateBase {
...
@Override
public void processConnectionEvent(Message message, int state) {
switch (state) {
...
case HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED://走了一系列流程后进入该case
stateLogD("SLC connected");
transitionTo(mConnected);//调用该方法进入下一个阶段
break;
...
}
}
...
}
这里的processConnectionEvent
方法的state值是从HeadsetNativeInterface#onConnectionStateChanged
开始调用底层,携带state数据,再回调上层来,接着再根据state来走case是否进入mConnected阶段
每个阶段差不多都是这样的流程,这里就不一一去分析了,直接看AudioConnecting
阶段
这个阶段就是我遇到问题的一个阶段,由于state值导致无法进入下一阶段,上代码
//各阶段常量
static final int CONNECT = 1;
static final int DISCONNECT = 2;
static final int CONNECT_AUDIO = 3;//注意
static final int DISCONNECT_AUDIO = 4;
class AudioConnecting extends ConnectedBase {
@Override
int getAudioStateInt() {
return BluetoothHeadset.STATE_AUDIO_CONNECTING; //记住这段代码,它的值为11
}
@Override
public void enter() {
super.enter();
sendMessageDelayed(CONNECT_TIMEOUT, mDevice, sConnectTimeoutMs);//发送消息给状态机的SmHandler-设置超时,延时发送和当前状态信息
broadcastStateTransitions();//发送广播给BtHleper.java,该方法稍后分析
}
@Override
public boolean processMessage(Message message) {
/// M: add log. @{
stateLogD("processMessage:" + message.what);//
/// @}
switch (message.what) {//该阶段的msg.what == 3,也就对应case CONNECT_AUDIO
case CONNECT:
case DISCONNECT:
case CONNECT_AUDIO:
case DISCONNECT_AUDIO:
deferMessage(message);//把msg放进消息队列
break;
case CONNECT_TIMEOUT: {//超时处理,我调试时没走该段代码
BluetoothDevice device = (BluetoothDevice) message.obj;
if (!mDevice.equals(device)) {
stateLogW("CONNECT_TIMEOUT for unknown device " + device);
break;
}
stateLogW("CONNECT_TIMEOUT");
transitionTo(mConnected);
break;
}
default: //当没有匹配的case时
return super.processMessage(message);
}
return HANDLED;
}
@Override
public void processAudioEvent(int state) {
switch (state) {
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTED:
stateLogW("processAudioEvent: audio connection failed");
transitionTo(mConnected);
break;
case HeadsetHalConstants.AUDIO_STATE_CONNECTING://目前匹配的该条case;常量值:1
// ignore, already in audio connecting state
break;
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTING:
// ignore, there is no BluetoothHeadset.STATE_AUDIO_DISCONNECTING
break;
case HeadsetHalConstants.AUDIO_STATE_CONNECTED://如果case走这一步就一切正常,能进行状态切换;常量值:2
stateLogI("processAudioEvent: audio connected");
transitionTo(mAudioOn);
break;
default:
stateLogE("processAudioEvent: bad state: " + state);
break;
}
}
@Override
public void exit() {
removeMessages(CONNECT_TIMEOUT);
super.exit();
}
}
进入该阶段时,调用processMessage
方法,会调用N次,第一次为msg.what == 3是正常的,看log日志后发现还有101的值传过来,该方法里匹配不到则走了default,导致循环调用,最后走到了processAudioEvent
,state为1,产生的异常,那么这个101的值代表什么呢?又从哪里进来的?带着这个疑问继续跟踪代码
static final int STACK_EVENT = 101; //看该常量的意思,应该是发送粘性事件
发现它在HeadsetService.java
中被调用了,跟着走,上代码
/**
* Handle messages from native (JNI) to Java. This needs to be synchronized to avoid posting
* messages to state machine before start() is done
*
* @param stackEvent event from native stack
*/
void messageFromNative(HeadsetStackEvent stackEvent) {
Objects.requireNonNull(stackEvent.device,
"Device should never be null, event: " + stackEvent);
synchronized (mStateMachines) {
HeadsetStateMachine stateMachine = mStateMachines.get(stackEvent.device);
if (stackEvent.type == HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
switch (stackEvent.valueInt) {
case HeadsetHalConstants.CONNECTION_STATE_CONNECTED:
case HeadsetHalConstants.CONNECTION_STATE_CONNECTING: {
// Create new state machine if none is found
if (stateMachine == null) {
stateMachine = HeadsetObjectsFactory.getInstance()
.makeStateMachine(stackEvent.device,
mStateMachinesThread.getLooper(), this, mAdapterService,
mNativeInterface, mSystemInterface);
mStateMachines.put(stackEvent.device, stateMachine);
}
break;
}
}
}
if (stateMachine == null) {
throw new IllegalStateException(
"State machine not found for stack event: " + stackEvent);
}
//101值在这里进行调用了,发送到AudioConnecting阶段
stateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT, stackEvent);
}
}
注意看最开始的官方注释,这是一套JNI代码,说明这里肯定是走了底层;我又有两个疑惑了
messageFromNative
方法带着这两个问题,又继续出发,看看messageFromNative
是在哪里被调用的,发现在HeadsetNativeInterface.java#sendMessageToService
中去调用了,而且不止一处被调用,很多JNI相关的方法都执行了该方法,具体来看看一段截图
以上这些方法都是底层回调数据到上层的方法,这下刚刚的疑惑就得到解决了,那么这个101状态值肯定是必须要走的,期间把event作为传入传递过去了,其中event就包含了state和type信息,记住此时的state为1,type为EVENT_TYPE_AUDIO_STATE_CHANGED
,至此源头就找到了,接着回到AudioConnecting
连接状态,看如下片段代码
@Override
public boolean processMessage(Message message) {
/// M: add log. @{
stateLogD("processMessage:" + message.what);// 此时这里的what已经是101了
/// @}
switch (message.what) {
case CONNECT:
case DISCONNECT:
case CONNECT_AUDIO:
case DISCONNECT_AUDIO:
deferMessage(message);
break;
case CONNECT_TIMEOUT: {
BluetoothDevice device = (BluetoothDevice) message.obj;
if (!mDevice.equals(device)) {
stateLogW("CONNECT_TIMEOUT for unknown device " + device);
break;
}
stateLogW("CONNECT_TIMEOUT");
transitionTo(mConnected);
break;
}
default: //因为没有101常量值匹配,走default
return super.processMessage(message);
}
return HANDLED;
}
再看看super.processMessage(msg);
上代码
/**
* Handle common messages in connected states. However, state specific messages must be
* handled individually.
*
* @param message Incoming message to handle
* @return True if handled successfully, False otherwise
*/
@Override
public boolean processMessage(Message message) {
switch (message.what) {
...
case STACK_EVENT: //此时走了该case,因为子类把message也传过来了
HeadsetStackEvent event = (HeadsetStackEvent) message.obj;
stateLogD("STACK_EVENT: " + event);
if (!mDevice.equals(event.device)) {
stateLogE("Event device does not match currentDevice[" + mDevice
+ "], event: " + event);
break;
}
switch (event.type) { //拿到底层给的event数据
case HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
processConnectionEvent(message, event.valueInt);
break;
case HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED://正好type值匹配的该条case
processAudioEvent(event.valueInt); //该方法是AudioConnecting连接阶段的一个回调方法,而event.valueInt == 1,也就是state == 1
break;
...
}
break;
default:
stateLogE("Unexpected msg " + getMessageName(message.what) + ": " + message);
return NOT_HANDLED;
}
return HANDLED;
}
这时回到了AudioConnecting
连接状态的processAudioEvent方法,上面已经提到过了,再来看一遍
@Override
public void processAudioEvent(int state) {//此时为1
switch (state) {
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTED:
stateLogW("processAudioEvent: audio connection failed");
transitionTo(mConnected);
break;
case HeadsetHalConstants.AUDIO_STATE_CONNECTING://该常量值:1
// ignore, already in audio connecting state
break;
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTING:
// ignore, there is no BluetoothHeadset.STATE_AUDIO_DISCONNECTING
break;
case HeadsetHalConstants.AUDIO_STATE_CONNECTED://该常量值:2
stateLogI("processAudioEvent: audio connected");
transitionTo(mAudioOn);
break;
default:
stateLogE("processAudioEvent: bad state: " + state);
break;
}
}
而刚好
state为1,走的 case HeadsetHalConstants.AUDIO_STATE_CONNECTING
,导致直接break;什么事情都没做,如果此时state为2,则进入case HeadsetHalConstants.AUDIO_STATE_CONNECTED
,再调用transitionTo(mAudioOn);
进行状态切换,就一切正常了,那么基于之前的一些疑问,在这里就已经解决了
分析完上层,接着看底层是怎么把state值回调到上层的。
com_android_bluetooth_hfp.cpp
void AudioStateCallback(bluetooth::headset::bthf_audio_state_t state,
RawAddress* bd_addr) override {
ALOGI("%s, %d for %s", __func__, state, bd_addr->ToString().c_str());
std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid() || !mCallbacksObj) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
// (jint)state 该值将回调到上层
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onAudioStateChanged,
(jint)state, addr.get());
}
接着跟踪state值怎么拿到的
btif_hf.cc
bt_status_t HeadsetInterface::ConnectAudio(RawAddress* bd_addr) {
CHECK_BTHF_INIT();
int idx = btif_hf_idx_by_bdaddr(bd_addr);
if ((idx < 0) || (idx >= BTA_AG_MAX_NUM_CLIENTS)) {
BTIF_TRACE_ERROR("%s: Invalid index %d", __func__, idx);
return BT_STATUS_FAIL;
}
/* Check if SLC is connected */
if (!IsSlcConnected(bd_addr)) {
LOG(ERROR) << ": SLC not connected for " << *bd_addr;
return BT_STATUS_NOT_READY;
}
// 注意看
do_in_jni_thread(base::Bind(&Callbacks::AudioStateCallback,
// Manual pointer management for now
base::Unretained(bt_hf_callbacks),
BTHF_AUDIO_STATE_CONNECTING,//该值就是state,而恰好这个常量就是1,上层也是卡在connecting无法执行
&btif_hf_cb[idx].connected_bda));
BTA_AgAudioOpen(btif_hf_cb[idx].handle);
return BT_STATUS_SUCCESS;
}
BTHF_AUDIO_STATE_CONNECTING
该值就是state =1 ,说明还没执行完就在这卡住了,所以上层也会卡在connecting。如果继续执行的话 --看代码
static void btif_hf_upstreams_evt(uint16_t event, char* p_param) {
...
case BTA_AG_WBS_EVT: //先走这条case,用于编解码音频
BTIF_TRACE_DEBUG(
"BTA_AG_WBS_EVT Set codec status %d codec %d 1=CVSD 2=MSBC",
p_data->val.hdr.status, p_data->val.num);
//如果p_data->val.num = 1则执行CVSD编码,为2的话走MSBC编码
if (p_data->val.num == BTA_AG_CODEC_CVSD) {
bt_hf_callbacks->WbsCallback(BTHF_WBS_NO,
&btif_hf_cb[idx].connected_bda);
} else if (p_data->val.num == BTA_AG_CODEC_MSBC) {
bt_hf_callbacks->WbsCallback(BTHF_WBS_YES,
&btif_hf_cb[idx].connected_bda);
} else {
bt_hf_callbacks->WbsCallback(BTHF_WBS_NONE,
&btif_hf_cb[idx].connected_bda);
}
break;
...
case BTA_AG_AUDIO_OPEN_EVT://然后走这条case,把state=2回调给上层
bt_hf_callbacks->AudioStateCallback(BTHF_AUDIO_STATE_CONNECTED,
&btif_hf_cb[idx].connected_bda);
break;
...
}
btif_hf_upstreams_evt
方法会被多次执行,具体怎么执行的暂不清楚,上面很关键的两条case,选择音频编解码和state=2回调到上层,但是我遇到的问题就是只走了编解码,state=2的case没有执行
至此 整个流程差不多就要结束了,回到上层来,既然状态机的各个阶段都正常的话,是不是该把广播发出去,通知各个方法。接着看到broadcastStateTransitions()方法,它在每个状态机阶段都会执行一次,发送不同阶段的广播以便于知道执行到哪个阶段了
void broadcastStateTransitions() {
if (mPrevState == null) {
return;
}
// TODO: Add STATE_AUDIO_DISCONNECTING constant to get rid of the 2nd part of this logic
if (getAudioStateInt() != mPrevState.getAudioStateInt() || (
mPrevState instanceof AudioDisconnecting && this instanceof AudioOn)) {
stateLogD("audio state changed: " + mDevice + ": " + mPrevState + " -> " + this);
broadcastAudioState(mDevice, mPrevState.getAudioStateInt(), getAudioStateInt());//发送当前的device和上一个状态和当前状态,这个状态指的是每个状态机的 getAudioStateInt 方法返回的值 ,AudioConnecting状态机的返回值=11,AudioOn状态机的返回值=12
}
if (getConnectionStateInt() != mPrevState.getConnectionStateInt()) {
stateLogD(
"connection state changed: " + mDevice + ": " + mPrevState + " -> " + this);
broadcastConnectionState(mDevice, mPrevState.getConnectionStateInt(),
getConnectionStateInt());
}
}
//发送广播
void broadcastAudioState(BluetoothDevice device, int fromState, int toState) {
stateLogD("broadcastAudioState: " + device + ": " + fromState + "->" + toState);
StatsLog.write(StatsLog.BLUETOOTH_SCO_CONNECTION_STATE_CHANGED,
mAdapterService.obfuscateAddress(device),
getConnectionStateFromAudioState(toState),
TextUtils.equals(mAudioParams.get(HEADSET_WBS), HEADSET_AUDIO_FEATURE_ON)
? BluetoothHfpProtoEnums.SCO_CODEC_MSBC
: BluetoothHfpProtoEnums.SCO_CODEC_CVSD);
mHeadsetService.onAudioStateChangedFromStateMachine(device, fromState, toState);
Intent intent = new Intent(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
//写入当前state状态
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, fromState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, toState);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
mHeadsetService.sendBroadcastAsUser(intent, UserHandle.ALL,
HeadsetService.BLUETOOTH_PERM);
}
该广播会在BtHelper.java#receiveBtEvent中接收
@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ synchronized void receiveBtEvent(Intent intent) {
final String action = intent.getAction();
if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
...
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
boolean broadcast = false;
int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);//拿到state值
// broadcast intent if the connection was initated by AudioService
...
switch (btState) {// 正常的话 state状态值为12
case BluetoothHeadset.STATE_AUDIO_CONNECTED://该值=12
scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;
if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
&& mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
}
//开启SCO服务,再执行startBluetoothScoOn
mDeviceBroker.setBluetoothScoOn(true, "BtHelper.receiveBtEvent");
break;
...
case BluetoothHeadset.STATE_AUDIO_CONNECTING://该值=11
if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
&& mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
}
break;
default:
// do not broadcast CONNECTING or invalid state
broadcast = false;
break;
}
//当state=12时,broadcast=true,代码没贴进来--作用是建立SCO服务
if (broadcast) {
broadcastScoConnectionState(scoAudioState);
...
Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);
newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, scoAudioState);
sendStickyBroadcastToAll(newIntent);
}
}
}
至此,全部流程结束
[根本原因]
state卡在了BTHF_AUDIO_STATE_CONNECTING,执行了音频编解码后没继续往后执行,根据log提示,SCO link无法响应,无法连接,导致SCO服务挂掉 进而产生的一些列异常;
[解决办法]
既然是SCO link无法响应,那就是Modem端无法给出正确的response;发现是蓝牙端本身的一个bug,需要合入MTK提供的俩个official patch
合入过后观察底层和上层的state值和状态机,发现一切正常,但是…还是通话无声…问题又来了…原因是要 enable merge interface ,那就来开启该宏
[音频无声解决办法]
1.ProjectConfig.mk # MTK_MERGE_INTERFACE_SUPPORT=yes
2.kernel-4.9/arch/arm64/configs/dl35_debug_defconfig # CONFIG_MTK_MERGE_INTERFACE_SUPPORT=y
3.kernel-4.9/arch/arm64/configs/dl35_defconfig # CONFIG_MTK_MERGE_INTERFACE_SUPPORT=y
至此 问题解决