车机蓝牙通话流程分析的流程分析
Posted Fresh_Air_Life
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了车机蓝牙通话流程分析的流程分析相关的知识,希望对你有一定的参考价值。
部分内容参照Android HeadSetClient端通话的传递_天花板之恋的博客-CSDN博客
android源代码中,如果通话状态有改变,会沿着这样的顺序传递:
蓝牙chip >> HCI接口 >> BlueDroid协议栈 >> Bluetooth >> 广播传递 >> Telecom ,
2. bluetooth 上层流程分析
2.1 收到JNI回调
通话状态有改变,会通过NativeInterface这个类里面的onCallSetup方法回调通知:
public class NativeInterface
.........
private void onCallSetup(int callsetup, byte[] address)
StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_CALLSETUP);
event.valueInt = callsetup;
event.device = getDevice(address);
HeadsetClientService service = HeadsetClientService.getHeadsetClientService();
service.messageFromNative(event); //1.通话状态改变的消息给到HeadsetClientService
........
我们以HF端发起拨号请求为例,那么最开始回调的状态就是CALL_STATE_DIALING,callsetup的值为2。然后NativeInterface会把消息封装成一个StackEvent类型的数据结构,给到HeadsetClientService去处理。
2.2 service 封装消息
在HeadsetClientService中,对于协议栈上来的数据,它只是做一个转接,会把消息给到对应的状态机HeadsetClientStateMachine处理:
public class HeadsetClientService extends ProfileService
........
public void messageFromNative(StackEvent stackEvent)
HeadsetClientStateMachine sm = getStateMachine(stackEvent.device);
sm.sendMessage(StackEvent.STACK_EVENT, stackEvent); //2.消息交给状态机处理
........
2.3状态机处理
在状态机中,此时状态正常情况下是处于Connected状态,看看对此消息的处理:
class Connected extends State
......
case StackEvent.EVENT_TYPE_CALLSETUP:
sendMessage(QUERY_CURRENT_CALLS); // 3.这里仅是给自己发一个QUERY_CURRENT_CALLS的消息
break;
好吧,这里状态机只是给自己发了一条QUERY_CURRENT_CALLS的消息,让自己去查询当前的通话状态:
case QUERY_CURRENT_CALLS:
removeMessages(QUERY_CURRENT_CALLS);
if (mCalls.size() > 0)
// If there are ongoing calls periodically check their status.
sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
//3.1这里值得注意,如果已经存在通话,那么就会定期查询通话状态
queryCallsStart();
break;
private boolean queryCallsStart()
clearPendingAction();
mNativeInterface.queryCurrentCalls(getByteAddress(mCurrentDevice)); //3.2调用JNI方法去查询当前的远程设备的通话
addQueuedAction(QUERY_CURRENT_CALLS, 0); //3.3这里会在mQueuedActions消息队列里添加一条记录
return true;
最后调用了NativeInterface提供的JNI方法,去查询对应设备的通话状态。
2.4 在执行了通话状态查询的请求指令,AG端反馈状态后,首先会回调NativeInterface的onCurrentCalls这个方法:
private void onCurrentCalls(int index, int dir, int state, int mparty, String number,
byte[] address)
StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_CURRENT_CALLS);
event.valueInt = index;
event.valueInt2 = dir;
event.valueInt3 = state;
event.valueInt4 = mparty;
event.valueString = number;
event.device = getDevice(address);
HeadsetClientService service = HeadsetClientService.getHeadsetClientService();
service.messageFromNative(event);
同样是给到HeadsetClientService,然后再给到HeadsetClientStateMachine处理:
class Connected extends State
......
case StackEvent.EVENT_TYPE_CURRENT_CALLS:
queryCallsUpdate(event.valueInt, event.valueInt3, event.valueString,
event.valueInt4 == HeadsetClientHalConstants.CALL_MPTY_TYPE_MULTI,
event.valueInt2 == HeadsetClientHalConstants.CALL_DIRECTION_OUTGOING);
break;
......
通话状态的查询结果处理,只是在mCallsUpdate 这个map中添加一个BluetoothHeadsetClientCall对象,没有再继续传递下去。
2.5
在onCurrentCalls这个方法之后,还会回调onCmdResult这个方法,它反馈的是HF端请求指令的执行结果,这里对应我们刚刚发起的查询当前通话状态的请求:
private void onCmdResult(int type, int cme, byte[] address)
StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_CMD_RESULT);
event.valueInt = type;
event.valueInt2 = cme;
event.device = getDevice(address);
HeadsetClientService service = HeadsetClientService.getHeadsetClientService();
service.messageFromNative(event);
同样是给到HeadsetClientService,然后再给到HeadsetClientStateMachine处理:
case StackEvent.EVENT_TYPE_CMD_RESULT:
Pair<Integer, Object> queuedAction = mQueuedActions.poll();
//从请求队列中取出队头的一条数据(之前我们往队列里放了数据)
switch (queuedAction.first)
case QUERY_CURRENT_CALLS:
queryCallsDone(); //代表查询所有通话的指令已经完成
break;
.......
break;
private void queryCallsDone()
......
// Add the new calls.
for (Integer idx : callAddedIds) //对于新增加的通话,会走到这里
BluetoothHeadsetClientCall c = mCallsUpdate.get(idx);
mCalls.put(idx, c); //把这个新增加通话放到mCalls里面
sendCallChangedIntent(c); //把通话改变的消息广播出去
private void sendCallChangedIntent(BluetoothHeadsetClientCall c) //把通话改变后的状态广播出去
Intent intent = new Intent(BluetoothHeadsetClient.ACTION_CALL_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.putExtra(BluetoothHeadsetClient.EXTRA_CALL, c);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
可以看到,最后的通话状态是通过广播的形式传递出去的,BluetoothHeadsetClientCall继承自Parcelable接口,是可以实现序列化传递的。
如果我们要实现一个蓝牙电话的功能,那么直接接收BluetoothHeadsetClient.ACTION_CALL_CHANGED这个广播就可以获取到通话的状态。
在Bluetooth内部,有一个HfpClientConnectionService的类,在HeadsetClientService初始化的时候,就会把它调起,如下:
// Start the HfpClientConnectionService to create connection with telecom when HFP
// connection is available.
Intent startIntent = new Intent(this, HfpClientConnectionService.class);
startService(startIntent);
可以看出,它是连接HFP和Telecom的桥梁。
在它的内部,也实现了BluetoothHeadsetClient.ACTION_CALL_CHANGED这个广播的接收处理,并且把状态传递给Telecom:
if (BluetoothHeadsetClient.ACTION_CALL_CHANGED.equals(action))
BluetoothHeadsetClientCall call =
intent.getParcelableExtra(BluetoothHeadsetClient.EXTRA_CALL);
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
HfpClientDeviceBlock block = findBlockForDevice(call.getDevice());
block.handleCall(call);
看到HfpClientDeviceBlock里面的handleCall方法:
synchronized void handleCall(BluetoothHeadsetClientCall call)
HfpClientConnection connection = findConnectionKey(call);
if (connection != null)
connection.updateCall(call);
connection.handleCallChanged(); //这里就通过之前创建的连接,去把通话改变后的状态给到Telecom
if (connection == null) //第一次的通话变化会走到这里
// Create the connection here, trigger Telecom to bind to us.
buildConnection(call, null);//创建一个连接
mTelecomManager.addNewUnknownCall(mPhoneAccount.getAccountHandle(), b);//告诉Telecom有新的通话
3.telecom 处理逻辑
1. telecomManager 开始处理事件
@SystemApi
public void addNewUnknownCall(PhoneAccountHandle phoneAccount, Bundle extras)
try
if (isServiceConnected())
getTelecomService().addNewUnknownCall(
phoneAccount, extras == null ? new Bundle() : extras);
catch (RemoteException e)
Log.e(TAG, "RemoteException adding a new unknown call: " + phoneAccount, e);
具体是实现在 TelecomServiceImpl
@Override
public void addNewUnknownCall(PhoneAccountHandle phoneAccountHandle, Bundle extras)
。。。
synchronized (mLock)
if (phoneAccountHandle != null &&
phoneAccountHandle.getComponentName() != null)
mAppOpsManager.checkPackage(
Binder.getCallingUid(),
phoneAccountHandle.getComponentName().getPackageName());
// Make sure it doesn't cross the UserHandle boundary
enforceUserHandleMatchesCaller(phoneAccountHandle);
enforcePhoneAccountIsRegisteredEnabled(phoneAccountHandle,
Binder.getCallingUserHandle());
long token = Binder.clearCallingIdentity();
try
Intent intent = new Intent(TelecomManager.ACTION_NEW_UNKNOWN_CALL);
if (extras != null)
extras.setDefusable(true);
intent.putExtras(extras);
intent.putExtra(CallIntentProcessor.KEY_IS_UNKNOWN_CALL, true);
intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
phoneAccountHandle);
// 发送intent
mCallIntentProcessorAdapter.processUnknownCallIntent(mCallsManager, intent);
finally
Binder.restoreCallingIdentity(token);
else
Log.i(this,
"Null phoneAccountHandle or not initiated by Telephony. " +
"Ignoring request to add new unknown call.");
。。。
CallIntentProcessor 将事件交给 callmanager 处理。
static void processUnknownCallIntent(CallsManager callsManager, Intent intent)
PhoneAccountHandle phoneAccountHandle = intent.getParcelableExtra(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
if (phoneAccountHandle == null)
Log.w(CallIntentProcessor.class, "Rejecting unknown call due to null phone account");
return;
if (phoneAccountHandle.getComponentName() == null)
Log.w(CallIntentProcessor.class, "Rejecting unknown call due to null component name");
return;
callsManager.addNewUnknownCall(phoneAccountHandle, intent.getExtras());
callManager 会新建一个call 事件,
void addNewUnknownCall(PhoneAccountHandle phoneAccountHandle, Bundle extras)
Uri handle = extras.getParcelable(TelecomManager.EXTRA_UNKNOWN_CALL_HANDLE);
Log.i(this, "addNewUnknownCall with handle: %s", Log.pii(handle));
Call call = new Call(
getNextCallId(),
mContext,
this,
mLock,
mConnectionServiceRepository,
mPhoneNumberUtilsAdapter,
handle,
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
phoneAccountHandle,
Call.CALL_DIRECTION_UNKNOWN /* callDirection */,
// Use onCreateIncomingConnection in TelephonyConnectionService, so that we attach
// to the existing connection instead of trying to create a new one.
true /* forceAttachToExistingConnection */,
false, /* isConference */
mClockProxy);
call.initAnalytics();
setIntentExtrasAndStartTime(call, extras);
call.addListener(this);
call.startCreateConnection(mPhoneAccountRegistrar);
Call 会创建CreateConnectionProcessor 处理
void startCreateConnection(PhoneAccountRegistrar phoneAccountRegistrar)
if (mCreateConnectionProcessor != null)
Log.w(this, "mCreateConnectionProcessor in startCreateConnection is not null. This is" +
" due to a race between NewOutgoingCallIntentBroadcaster and " +
"phoneAccountSelected, but is harmlessly resolved by ignoring the second " +
"invocation.");
return;
mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this,
phoneAccountRegistrar, mContext);
mCreateConnectionProcessor.process();
CreateConnectionProcessor
public void process()
Log.v(this, "process");
clearTimeout();
mAttemptRecords = new ArrayList<>();
if (mCall.getTargetPhoneAccount() != null)
mAttemptRecords.add(new CallAttemptRecord(
mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount()));
if (!mCall.isSelfManaged())
adjustAttemptsForConnectionManager();
adjustAttemptsForEmergency(mCall.getTargetPhoneAccount());
mAttemptRecordIterator = mAttemptRecords.iterator();
attemptNextPhoneAccount();
private void attemptNextPhoneAccount()
Log.v(this, "attemptNextPhoneAccount");
CallAttemptRecord attempt = null;
if (mAttemptRecordIterator.hasNext())
attempt = mAttemptRecordIterator.next();
if (!mPhoneAccountRegistrar.phoneAccountRequiresBindPermission(
attempt.connectionManagerPhoneAccount))
Log.w(this,
"Connection mgr does not have BIND_TELECOM_CONNECTION_SERVICE for "
+ "attempt: %s", attempt);
attemptNextPhoneAccount();
return;
// If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it
// also requires the BIND_TELECOM_CONNECTION_SERVICE permission.
if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) &&
!mPhoneAccountRegistrar.phoneAccountRequiresBindPermission(
attempt.targetPhoneAccount))
Log.w(this,
"Target PhoneAccount does not have BIND_TELECOM_CONNECTION_SERVICE for "
+ "attempt: %s", attempt);
attemptNextPhoneAccount();
return;
if (mCallResponse != null && attempt != null)
Log.i(this, "Trying attempt %s", attempt);
PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount;
mService = mRepository.getService(phoneAccount.getComponentName(),
phoneAccount.getUserHandle());
if (mService == null)
Log.i(this, "Found no connection service for attempt %s", attempt);
attemptNextPhoneAccount();
else
mConnectionAttempt++;
mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
mCall.setConnectionService(mService);
setTimeoutIfNeeded(mService, attempt);
if (mCall.isIncoming())
mService.createConnection(mCall, CreateConnectionProcessor.this);
else
// Start to create the connection for outgoing call after the ConnectionService
// of the call has gained the focus.
mCall.getConnectionServiceFocusManager().requestFocus(
mCall,
new CallsManager.RequestCallback(new CallsManager.PendingAction()
@Override
public void performAction()
Log.d(this, "perform create connection");
mService.createConnection(
mCall,
CreateConnectionProcessor.this);
));
else
Log.v(this, "attemptNextPhoneAccount, no more accounts, failing");
DisconnectCause disconnectCause = mLastErrorDisconnectCause != null ?
mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR);
notifyCallConnectionFailure(disconnectCause);
???????????????
ConnectionServiceWrapper 这里会执行createConnection 操作
@Override
public void handleCreateConnectionComplete(String callId, ConnectionRequest request,
ParcelableConnection connection, Session.Info sessionInfo)
Log.startSession(sessionInfo, LogUtils.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE);
long token = Binder.clearCallingIdentity();
try
synchronized (mLock)
logIncoming("handleCreateConnectionComplete %s", callId);
ConnectionServiceWrapper.this
.handleCreateConnectionComplete(callId, request, connection);
if (mServiceInterface != null)
logOutgoing("createConnectionComplete %s", callId);
try
mServiceInterface.createConnectionComplete(callId,
Log.getExternalSession());
catch (RemoteException e)
catch (Throwable t)
Log.e(ConnectionServiceWrapper.this, t, "");
throw t;
finally
Binder.restoreCallingIdentity(token);
Log.endSession();
调用Call.java 的handleCreateConnectionSuccess
@Override
public void handleCreateConnectionSuccess(
CallIdMapper idMapper,
ParcelableConnection connection)
Log.d(this, "handleCreateConnectionSuccessful %s", connection);
setTargetPhoneAccount(connection.getPhoneAccount());
setHandle(connection.getHandle(), connection.getHandlePresentation());
setCallerDisplayName(
connection.getCallerDisplayName(), connection.getCallerDisplayNamePresentation());
setConnectionCapabilities(connection.getConnectionCapabilities());
setConnectionProperties(connection.getConnectionProperties());
setIsVoipAudioMode(connection.getIsVoipAudioMode());
setSupportedAudioRoutes(connection.getSupportedAudioRoutes());
setVideoProvider(connection.getVideoProvider());
setVideoState(connection.getVideoState());
setRingbackRequested(connection.isRingbackRequested());
setStatusHints(connection.getStatusHints());
putExtras(SOURCE_CONNECTION_SERVICE, connection.getExtras());
mConferenceableCalls.clear();
for (String id : connection.getConferenceableConnectionIds())
mConferenceableCalls.add(idMapper.getCall(id));
switch (mCallDirection)
case CALL_DIRECTION_INCOMING:
// Listeners (just CallsManager for now) will be responsible for checking whether
// the call should be blocked.
for (Listener l : mListeners)
l.onSuccessfulIncomingCall(this);
break;
case CALL_DIRECTION_OUTGOING:
for (Listener l : mListeners)
l.onSuccessfulOutgoingCall(this,
getStateFromConnectionState(connection.getState()));
break;
case CALL_DIRECTION_UNKNOWN:
for (Listener l : mListeners)
l.onSuccessfulUnknownCall(this, getStateFromConnectionState(connection
.getState()));
break;
接着 CallsManager 的 onSuccessfulUnknownCall() 到 addCall()
public void addCall(Call call)
Trace.beginSection("addCall");
Log.d(this, "addCall(%s)", call);
call.addListener(this);
mCalls.add(call);
// Specifies the time telecom finished routing the call. This is used by the dialer for
// analytics.
Bundle extras = call.getIntentExtras();
extras.putLong(TelecomManager.EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS,
SystemClock.elapsedRealtime());
updateCanAddCall();
// onCallAdded for calls which immediately take the foreground (like the first call).
for (CallsManagerListener listener : mListeners)
if (LogUtils.SYSTRACE_DEBUG)
Trace.beginSection(listener.getClass().toString() + " addCall");
listener.onCallAdded(call);
if (LogUtils.SYSTRACE_DEBUG)
Trace.endSection();
Trace.endSection();
InCallController 这个对象是连接类
@Override
public void onCallAdded(Call call)
if (!isBoundAndConnectedToServices())
Log.i(this, "onCallAdded: %s; not bound or connected.", call);
// We are not bound, or we're not connected.
bindToServices(call);
else
// We are bound, and we are connected.
adjustServiceBindingsForEmergency();
// This is in case an emergency call is added while there is an existing call.
mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(call,
mCallsManager.getCurrentUserHandle());
Log.i(this, "onCallAdded: %s", call);
// Track the call if we don't already know about it.
addCall(call);
Log.i(this, "mInCallServiceConnection isConnected=%b",
mInCallServiceConnection.isConnected());
List<ComponentName> componentsUpdated = new ArrayList<>();
for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.entrySet())
InCallServiceInfo info = entry.getKey();
if (call.isExternalCall() && !info.isExternalCallsSupported())
continue;
if (call.isSelfManaged() && !info.isSelfManagedCallsSupported())
continue;
// Only send the RTT call if it's a UI in-call service
boolean includeRttCall = info.equals(mInCallServiceConnection.getInfo());
componentsUpdated.add(info.getComponentName());
IInCallService inCallService = entry.getValue();
ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar(),
info.isExternalCallsSupported(), includeRttCall,
info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI);
try
inCallService.addCall(parcelableCall);
catch (RemoteException ignored)
Log.i(this, "Call added to components: %s", componentsUpdated);
其中 bindToServices
public void bindToServices(Call call)
if (mInCallServiceConnection == null)
InCallServiceConnection dialerInCall = null;
InCallServiceInfo defaultDialerComponentInfo = getDefaultDialerComponent();
Log.i(this, "defaultDialer: " + defaultDialerComponentInfo);
if (defaultDialerComponentInfo != null &&
!defaultDialerComponentInfo.getComponentName().equals(
mSystemInCallComponentName))
dialerInCall = new InCallServiceBindingConnection(defaultDialerComponentInfo);
Log.i(this, "defaultDialer: " + dialerInCall);
InCallServiceInfo systemInCallInfo = getInCallServiceComponent(
mSystemInCallComponentName, IN_CALL_SERVICE_TYPE_SYSTEM_UI);
EmergencyInCallServiceConnection systemInCall =
new EmergencyInCallServiceConnection(systemInCallInfo, dialerInCall);
systemInCall.setHasEmergency(mCallsManager.hasEmergencyCall());
InCallServiceConnection carModeInCall = null;
InCallServiceInfo carModeComponentInfo = getCarModeComponent();
if (carModeComponentInfo != null &&
!carModeComponentInfo.getComponentName().equals(mSystemInCallComponentName))
carModeInCall = new InCallServiceBindingConnection(carModeComponentInfo);
mInCallServiceConnection =
new CarSwappingInCallServiceConnection(systemInCall, carModeInCall);
mInCallServiceConnection.setCarMode(shouldUseCarModeUI());
// Actually try binding to the UI InCallService. If the response
if (mInCallServiceConnection.connect(call) ==
InCallServiceConnection.CONNECTION_SUCCEEDED)
// Only connect to the non-ui InCallServices if we actually connected to the main UI
// one.
connectToNonUiInCallServices(call);
mBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false,
mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
mContext.getContentResolver()),
TimeUnit.MILLISECONDS);
else
Log.i(this, "bindToServices: current UI doesn't support call; not binding.");
InCallController
// 这个方法会绑定默认的电话应用
public void bindToServices(Call call)
if (mInCallServiceConnection == null)
InCallServiceConnection dialerInCall = null;
InCallServiceInfo defaultDialerComponentInfo = getDefaultDialerComponent();
Log.i(this, "defaultDialer: " + defaultDialerComponentInfo);
if (defaultDialerComponentInfo != null &&
!defaultDialerComponentInfo.getComponentName().equals(
mSystemInCallComponentName))
dialerInCall = new InCallServiceBindingConnection(defaultDialerComponentInfo);
Log.i(this, "defaultDialer: " + dialerInCall);
InCallServiceInfo systemInCallInfo = getInCallServiceComponent(
mSystemInCallComponentName, IN_CALL_SERVICE_TYPE_SYSTEM_UI);
EmergencyInCallServiceConnection systemInCall =
new EmergencyInCallServiceConnection(systemInCallInfo, dialerInCall);
systemInCall.setHasEmergency(mCallsManager.hasEmergencyCall());
InCallServiceConnection carModeInCall = null;
InCallServiceInfo carModeComponentInfo = getCarModeComponent();
if (carModeComponentInfo != null &&
!carModeComponentInfo.getComponentName().equals(mSystemInCallComponentName))
carModeInCall = new InCallServiceBindingConnection(carModeComponentInfo);
mInCallServiceConnection =
new CarSwappingInCallServiceConnection(systemInCall, carModeInCall);
mInCallServiceConnection.setCarMode(shouldUseCarModeUI());
// Actually try binding to the UI InCallService. If the response
if (mInCallServiceConnection.connect(call) ==
InCallServiceConnection.CONNECTION_SUCCEEDED)
// Only connect to the non-ui InCallServices if we actually connected to the main UI
// one.
connectToNonUiInCallServices(call);
mBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false,
mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
mContext.getContentResolver()),
TimeUnit.MILLISECONDS);
else
Log.i(this, "bindToServices: current UI doesn't support call; not binding.");
走到这里看到有个defaultDialerApp 这个就是默认的电话应用
以上是关于车机蓝牙通话流程分析的流程分析的主要内容,如果未能解决你的问题,请参考以下文章
Android博通BCM libbt-vendor.so 分析蓝牙初始化流程