当前位置: 首页 > news >正文

【android bluetooth 协议分析 11】【AVDTP详解 2】【avdtp 初始化阶段主要回调关系梳理】

在车机中 a2dp 通常情况下作为 sink. 本篇来帮助各位 朋友梳理一下,这部分的初始化流程。

我们着重梳理 native 层的逻辑, framework - java 侧一般比较容易看明白, 暂时不做梳理。 如果需要笨叔梳理的可以在博客评论。 出专门的章节来梳理。

本篇的目的只有一个,在 初始化流程中 梳理出 avdtp 在 l2cap 层的回调接口。 这样我们就可以 从 l2cap 层, 的接口回调中,配合具体案例,来分析 avdtp 的所有内容。

1. java侧触发a2dpsink协议栈初始化

蓝牙进程在拉起来的时候, 回去加载 A2dpSinkNativeInterface 类,此时就会触发 对应的 classInitNative 调用。

当 A2dpSinkNativeInterface.java 调用 加载时,会调用 c++ 的 classInitNative,

// android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface.java
public class A2dpSinkNativeInterface {private static final String TAG = "A2dpSinkNativeInterface";private static final boolean DBG = A2dpSinkService.DBG;//Log.isLoggable(TAG, Log.DEBUG);private AdapterService mAdapterService;@GuardedBy("INSTANCE_LOCK")private static A2dpSinkNativeInterface sInstance;private static final Object INSTANCE_LOCK = new Object();static {classInitNative();}

// android/app/jni/com_android_bluetooth_a2dp_sink.cppstatic JNINativeMethod sMethods[] = {{"classInitNative", "()V", (void*)classInitNative},{"initNative", "(I)V", (void*)initNative},{"cleanupNative", "()V", (void*)cleanupNative},{"connectA2dpNative", "([B)Z", (void*)connectA2dpNative},{"disconnectA2dpNative", "([B)Z", (void*)disconnectA2dpNative},{"informAudioFocusStateNative", "(I)V", (void*)informAudioFocusStateNative},{"informAudioTrackGainNative", "(F)V", (void*)informAudioTrackGainNative},{"setActiveDeviceNative", "([B)Z", (void*)setActiveDeviceNative},
};int register_com_android_bluetooth_a2dp_sink(JNIEnv* env) {return jniRegisterNativeMethods(env, "com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface", sMethods,NELEM(sMethods));
}static void classInitNative(JNIEnv* env, jclass clazz) {method_onConnectionStateChanged =env->GetMethodID(clazz, "onConnectionStateChanged", "([BI)V");method_onAudioStateChanged =env->GetMethodID(clazz, "onAudioStateChanged", "([BI)V");method_onAudioConfigChanged =env->GetMethodID(clazz, "onAudioConfigChanged", "([BII)V");ALOGI("%s: succeeds", __func__);
}
  • 在 classInitNative 中我们只是 获取了 几个java 层的回调函数:
    • onConnectionStateChanged
    • onAudioStateChanged
    • onAudioConfigChanged

当A2dpSinkService 启动时, 才真正的触发 调用 a2dpsink 协议栈部分的初始化

// android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkService.javaprotected boolean start() {mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),"AdapterService cannot be null when A2dpSinkService starts");mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(),"DatabaseManager cannot be null when A2dpSinkService starts");mNativeInterface = A2dpSinkNativeInterface.getInstance();mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices();mNativeInterface.init(mMaxConnectedAudioDevices); // 这里才真正的触发 调用 a2dpsink 协议栈初始化synchronized (mStreamHandlerLock) {mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, mNativeInterface);}

// android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface.javapublic void init(int maxConnectedAudioDevices) {initNative(maxConnectedAudioDevices); // 这个是触发 a2dp sink 协议栈初始化的点。}
// android/app/jni/com_android_bluetooth_a2dp_sink.cpp
static void initNative(JNIEnv* env, jobject object,jint maxConnectedAudioDevices) {...sBluetoothA2dpInterface =(btav_sink_interface_t*)btInf->get_profile_interface(BT_PROFILE_ADVANCED_AUDIO_SINK_ID);// 这里实际触发到了 btif_av.cc::init_sinkbt_status_t status = sBluetoothA2dpInterface->init(&sBluetoothA2dpCallbacks,maxConnectedAudioDevices);
...
}

上面已经看清楚了触发时机,那就是 当A2dpSinkService 启动时,触发 调用 a2dpsink 协议栈部分的初始化。

2. btif 侧初始化

为啥触发到 btif_av.cc::init_sink

// system/btif/src/bluetooth.cc
static const void* get_profile_interface(const char* profile_id) {
...if (is_profile(profile_id, BT_PROFILE_ADVANCED_AUDIO_SINK_ID))return btif_av_get_sink_interface();
}// system/btif/src/btif_av.cc
const btav_sink_interface_t* btif_av_get_sink_interface(void) {BTIF_TRACE_EVENT("%s", __func__);return &bt_av_sink_interface;
}// system/btif/src/btif_av.cc
static const btav_sink_interface_t bt_av_sink_interface = {sizeof(btav_sink_interface_t),init_sink, // 这里会触发初始化sink_connect_src,sink_disconnect_src,cleanup_sink,update_audio_focus_state,update_audio_track_gain,sink_set_active_device};
// system/btif/src/btif_av.ccstatic BtifAvSink btif_av_sink;static bt_status_t init_sink(btav_sink_callbacks_t* callbacks,int max_connected_audio_devices) {BTIF_TRACE_EVENT("%s", __func__);return btif_av_sink.Init(callbacks, max_connected_audio_devices);
}
  • btif_av_sink 是一个静态变量,也就是说, 一个蓝牙进程中,只能有一个 BtifAvSink

3. btif_enable_service


// system/btif/src/btif_av.cc
bt_status_t BtifAvSink::Init(btav_sink_callbacks_t* callbacks,int max_connected_audio_devices) {...btif_enable_service(BTA_A2DP_SINK_SERVICE_ID); // 这里去使能  a2dp_sinkenabled_ = true;return BT_STATUS_SUCCESS;
}

这里当地插一句, 我们 协议栈侧 的native 服务 ,基本是通过 btif_enable_service 函数来 使能的。

  1. btif_enable_service(BTA_A2DP_SOURCE_SERVICE_ID)
  2. btif_enable_service(BTA_A2DP_SINK_SERVICE_ID)
  3. btif_enable_service(BTA_HIDD_SERVICE_ID)
  4. btif_enable_service(BTA_HFP_HS_SERVICE_ID)
  5. btif_enable_service(BTA_HFP_SERVICE_ID)
  6. btif_enable_service(BTA_HID_SERVICE_ID)
  7. btif_enable_service(BTA_SDP_SERVICE_ID)
// system/bta/include/bta_api.h
#define BTA_A2DP_SINK_SERVICE_ID 18  /* A2DP Sink */// system/btif/src/btif_core.cc
void btif_enable_service(tBTA_SERVICE_ID service_id) {btif_enabled_services |= (1 << service_id); // 记录当前启动的服务BTIF_TRACE_DEBUG("%s: current services:0x%x", __func__,btif_enabled_services);if (btif_is_enabled()) {btif_dm_enable_service(service_id, true);}
}// system/btif/src/btif_dm.cc
/*
* 这个函数的目的是:在底层启用或关闭某个 Bluetooth Profile 服务,并在启用成功后,将本地支持的 UUID 列表更新到上层(Java 层),以便系统知道当前本机支持哪些蓝牙 Profile。
*/
void btif_dm_enable_service(tBTA_SERVICE_ID service_id, bool enable) {bt_status_t status = btif_in_execute_service_request(service_id, enable); // 调用底层逻辑来开启或关闭实际的蓝牙服务。if (status == BT_STATUS_SUCCESS) {// 说明服务已经初始化成功,那么系统现在支持的 Profile 也发生了变化,我们就需要将这个变化通知 Java 层。bt_property_t property;Uuid local_uuids[BT_MAX_NUM_UUIDS];/* Now send the UUID_PROPERTY_CHANGED event to the upper layer */BTIF_STORAGE_FILL_PROPERTY(&property, BT_PROPERTY_UUIDS,sizeof(local_uuids), local_uuids);// 这一步只是初始化 `property`,下一步才真正获取 UUID。btif_storage_get_adapter_property(&property);// 这个函数负责从配置文件(如 `/data/misc/bluedroid/bt_config.conf`)中获取当前注册的 Profile UUID 列表,填充到 local_uuids 数组中。invoke_adapter_properties_cb(BT_STATUS_SUCCESS, 1, &property);// 调用 Java 层注册的 BluetoothAdapterPropertiesCallback 回调,让 Java 层知道:蓝牙支持的服务列表(UUID)更新了」,可以刷新界面或更新系统状态}return;
}

btif_dm_enable_service 函数主要做如下几件事情:

  1. 调用 btif_in_execute_service_request 开开启底层蓝牙服务
  2. 服务开启成功后, 会调用 invoke_adapter_properties_cb 通知给 java 层。 当前 xxx 服务已经启动。此时 app 层才能发起 对应的连接。例如 此时用户才能去连接 手机的a2dp.

最终 uuid 会上报到:AdapterProperties.mUuids 中

class AdapterProperties {private volatile ParcelUuid[] mUuids;ParcelUuid[] getUuids() {return mUuids;}void adapterPropertyChangedCallback(int[] types, byte[][] values) {Intent intent;int type;byte[] val;for (int i = 0; i < types.length; i++) {val = values[i];type = types[i];infoLog("adapterPropertyChangedCallback with type:" + type + " len:" + val.length);synchronized (mObject) {switch (type) {case AbstractionLayer.BT_PROPERTY_UUIDS:mUuids = Utils.byteArrayToUuid(val); // 最终 uuid 会上报到这里 break;
}}// 当应用层调用 connectAllEnabledProfiles 连接时, 才能去连接对应的服务。
// android/app/src/com/android/bluetooth/btservice/AdapterService.java
public int connectAllEnabledProfiles(BluetoothDevice device) {ParcelUuid[] localDeviceUuids = mAdapterProperties.getUuids();if (mA2dpService != null && isSupported(localDeviceUuids, remoteDeviceUuids,BluetoothProfile.A2DP, device)) {Log.i(TAG, "connectAllEnabledProfiles: Connecting A2dp");// Set connection policy also connects the profile with CONNECTION_POLICY_ALLOWEDmA2dpService.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED);numProfilesConnected++;}
}

4. btif_in_execute_service_request

我们接着去分析 btif_in_execute_service_request 看 a2dpsink 是如何启动的。

// system/btif/src/btif_dm.cc
bt_status_t btif_in_execute_service_request(tBTA_SERVICE_ID service_id,bool b_enable) {switch (service_id) {case BTA_A2DP_SINK_SERVICE_ID: {btif_av_sink_execute_service(b_enable);} break;
// system/btif/src/btif_av.cc
bt_status_t btif_av_sink_execute_service(bool enable) {BTIF_TRACE_EVENT("%s: Sink service: %s", __func__,(enable) ? "enable" : "disable");if (enable) {// Added BTA_AV_FEAT_NO_SCO_SSPD - this ensures that the BTA does not// auto-suspend AV streaming on AG events (SCO or Call). The suspend shall// be initiated by the app/audioflinger layers.tBTA_AV_FEAT features = BTA_AV_FEAT_NO_SCO_SSPD | BTA_AV_FEAT_RCCT |BTA_AV_FEAT_METADATA | BTA_AV_FEAT_VENDOR |BTA_AV_FEAT_ADV_CTRL | BTA_AV_FEAT_RCTG |BTA_AV_FEAT_BROWSE | BTA_AV_FEAT_COVER_ARTWORK;if (delay_reporting_enabled()) {features |= BTA_AV_FEAT_DELAY_RPT;}BTA_AvEnable(features, bta_av_sink_callback); // 这个地方会向 bta 层注册处理处理函数。btif_av_sink.RegisterAllBtaHandles(); // 我们还是着重分析一下 他return BT_STATUS_SUCCESS;}// Disable the servicebtif_av_sink.DeregisterAllBtaHandles();BTA_AvDisable();return BT_STATUS_SUCCESS;
}

1. BTA_AvEnable

// system/bta/av/bta_av_api.cc
void BTA_AvEnable(tBTA_AV_FEAT features, tBTA_AV_CBACK* p_cback) {tBTA_AV_API_ENABLE* p_buf =(tBTA_AV_API_ENABLE*)osi_malloc(sizeof(tBTA_AV_API_ENABLE));/* register with BTA system manager */bta_sys_register(BTA_ID_AV, &bta_av_reg); // 这样 jni 下来的数据, 就可以通过  bta_sys_sendmsg 下发给 bta_av_reg 中的函数处理了。p_buf->hdr.event = BTA_AV_API_ENABLE_EVT;p_buf->p_cback = p_cback;p_buf->features = features;bta_sys_sendmsg(p_buf); // bta av 第一个处理的事件就是 BTA_AV_API_ENABLE_EVT 事件
}static const tBTA_SYS_REG bta_av_reg = {bta_av_hdl_event, BTA_AvDisable};

2. bta_av_hdl_event

// system/bta/av/bta_av_main.cc
bool bta_av_hdl_event(BT_HDR_RIGID* p_msg) {if (p_msg->event > BTA_AV_LAST_EVT) { // 非法事件,直接忽略,释放消息return true; /* to free p_msg */}if (p_msg->event >= BTA_AV_FIRST_NSM_EVT) {/*非状态机事件(如 ENABLE、DISABLE)不依赖任何状态,比如:`BTA_AV_API_ENABLE_EVT`:初始化回调、注册模块`BTA_AV_API_DISABLE_EVT`:反注册模块*/APPL_TRACE_VERBOSE("%s: AV nsm event=0x%x(%s)", __func__, p_msg->event,bta_av_evt_code(p_msg->event));bta_av_non_state_machine_event(p_msg->event, (tBTA_AV_DATA*)p_msg);} else if (p_msg->event >= BTA_AV_FIRST_SM_EVT &&p_msg->event <= BTA_AV_LAST_SM_EVT) {/*主状态机事件(如控制流程)依赖全局状态 `bta_av_cb`管理连接、注册、发现等控制逻辑举例事件:BTA_AV_API_REGISTER_EVTBTA_AV_API_OPEN_EVT*/APPL_TRACE_VERBOSE("%s: AV sm event=0x%x(%s)", __func__, p_msg->event,bta_av_evt_code(p_msg->event));/* state machine events */bta_av_sm_execute(&bta_av_cb, p_msg->event, (tBTA_AV_DATA*)p_msg);} else {/*子状态机事件(具体媒体流)针对每一个媒体流连接(SCB)都有一个独立状态机可同时管理多个音频会话(比如双设备播放)举例事件:BTA_AV_STR_OPEN_EVT(Stream open)BTA_AV_STR_START_EVT(Start playback)BTA_AV_STR_CLOSE_EVT*/APPL_TRACE_VERBOSE("%s: bta_handle=0x%x", __func__, p_msg->layer_specific);/* stream state machine events */bta_av_ssm_execute(bta_av_hndl_to_scb(p_msg->layer_specific), p_msg->event,(tBTA_AV_DATA*)p_msg);}return true;
}

bta_av_hdl_event() 函数,是 A2DP 模块中 统一处理所有 BTA_AV 事件的分发器(dispatcher)。它根据事件的类型,将事件分发给不同的处理逻辑(函数)来处理 —— 本质上是 “职责分离 + 状态感知” 的策略。

1. 为什么同一个模块里会有“多个状态机处理函数”?

事件类型范围处理函数对应状态机/作用
BTA_AV_FIRST_NSM_EVT ~ BTA_AV_LAST_EVTbta_av_non_state_machine_event()全局事件(非状态相关),如 enable、disable
BTA_AV_FIRST_SM_EVT ~ BTA_AV_LAST_SM_EVTbta_av_sm_execute()主状态机(control block),处理如 register、discover、open
其它事件(不在以上两个范围)bta_av_ssm_execute()每个 stream 实例的状态机,例如一个 A2DP 连接的媒体流生命周期

2. 为什么要分这么细?

清晰职责分离

类型管理内容状态
Non-State-Machine模块初始化、功能开关无状态
State-Machine控制模块行为注册、发现、连接
Sub-State-Machine每路媒体流打开、播放、暂停
  1. 支持多设备连接

    • 比如同时连接两个 A2DP Source,每路 stream 都有自己的状态,不能混用一个状态机。
  2. 提高可维护性 & 扩展性

    • 模块功能多(如 A2DP Sink、Source、AVRCP Control/Target),事件分派机制让代码层次分明,易于拓展。

你可以把这个机制类比为:

  • 非状态事件:像家电开关按钮,按了就执行动作,不关心状态(比如插座上电)
  • 主状态机事件:像遥控器控制电视的主机:开机、切源、音量设置,主机来执行
  • 子状态机事件:像电视里每个“应用”(比如 YouTube、Netflix)内部自己管理“播放-暂停-退出”
问题解答
为何用不同处理函数?事件来源不同,作用域不同:有全局控制的,有媒体流专属的
如何区分处理?通过 event 范围判断,派发到对应函数
好处?解耦清晰、可支持多媒体流、模块化强

5. RegisterAllBtaHandles

我们继续分析一下 btif_av_sink.RegisterAllBtaHandles()

// system/btif/src/btif_av.ccvoid BtifAvSink::RegisterAllBtaHandles() {for (int peer_id = kPeerIdMin; peer_id < kPeerIdMax; peer_id++) {BTA_AvRegister(BTA_AV_CHNL_AUDIO, kBtifAvSinkServiceName.c_str(), peer_id,bta_av_sink_media_callback, UUID_SERVCLASS_AUDIO_SINK);}
}
// system/bta/av/bta_av_api.cc
void BTA_AvRegister(tBTA_AV_CHNL chnl, const char* p_service_name,uint8_t app_id, tBTA_AV_SINK_DATA_CBACK* p_sink_data_cback,uint16_t service_uuid) {tBTA_AV_API_REG* p_buf =(tBTA_AV_API_REG*)osi_malloc(sizeof(tBTA_AV_API_REG));p_buf->hdr.layer_specific = chnl;p_buf->hdr.event = BTA_AV_API_REGISTER_EVT; // 在 bta_av_hdl_event 中处理 BTA_AV_API_REGISTER_EVT 事件if (p_service_name)strlcpy(p_buf->p_service_name, p_service_name, BTA_SERVICE_NAME_LEN);elsep_buf->p_service_name[0] = 0;p_buf->app_id = app_id;p_buf->p_app_sink_data_cback = p_sink_data_cback;p_buf->service_uuid = service_uuid;bta_sys_sendmsg(p_buf);
}

6. BTA_AV_API_REGISTER_EVT 事件处理

  • BTA_AV_API_REGISTER_EVT 命令是一个 非状态机事件 ,所以在 bta_av_hdl_event 中直接就调用 bta_av_non_state_machine_event 处理了。
// system/bta/av/bta_av_main.cc
static void bta_av_non_state_machine_event(uint16_t event,tBTA_AV_DATA* p_data) {switch (event) {case BTA_AV_API_REGISTER_EVT:bta_av_api_register(p_data);break;
// system/bta/av/bta_av_main.cc
static void bta_av_api_register(tBTA_AV_DATA* p_data) {
...
if (bta_av_cb.reg_audio == 0) {/* the first channel registered. register to AVDTP */reg.ctrl_mtu = 672;reg.ret_tout = BTA_AV_RET_TOUT;reg.sig_tout = BTA_AV_SIG_TOUT;reg.idle_tout = BTA_AV_IDLE_TOUT;reg.scb_index = p_scb->hdi;bta_ar_reg_avdt(&reg, bta_av_conn_cback); // 这里我们终于看到注册 avdt 的地方。bta_sys_role_chg_register(&bta_av_sys_rs_cback);...}
...
}
// system/bta/ar/bta_ar.cc
void bta_ar_reg_avdt(AvdtpRcb* p_reg, tAVDT_CTRL_CBACK* p_cback) {bta_ar_cb.p_av_conn_cback = p_cback; // 将 bta_av_conn_cback 赋值给 p_av_conn_cbackif (bta_ar_cb.avdt_registered == 0) {AVDT_Register(p_reg, bta_ar_avdt_cback); // 注册 avdt} else {APPL_TRACE_WARNING("%s: doesn't register again (registered:%d)", __func__,bta_ar_cb.avdt_registered);}bta_ar_cb.avdt_registered |= BTA_AR_AV_MASK;
}

7. AVDT_Register


// system/stack/avdt/avdt_api.cc
void AVDT_Register(AvdtpRcb* p_reg, tAVDT_CTRL_CBACK* p_cback) {/* register PSM with L2CAP */L2CA_Register2(AVDT_PSM, avdt_l2c_appl, true /* enable_snoop */, nullptr,kAvdtpMtu, 0, BTA_SEC_AUTHENTICATE);/* initialize AVDTP data structures */avdt_scb_init(); // 初始化 流控制块avdt_ccb_init(); // 初始化 通道控制块avdt_ad_init();  // 初始化 适配层/* copy registration struct */avdtp_cb.rcb = *p_reg;avdtp_cb.p_conn_cback = p_cback; // bta_ar_avdt_cback ;    在 avdt_ccb_ll_opened 函数中会调用 p_conn_cback, 从而触发 bta_ar_avdt_cback 的调用 -> p_av_conn_cback(实际调用的是 bta_av_conn_cback)
}
  • 通过 L2CA_Register2 向 l2cap 注册 avdtp 相关的 l2cap 的回调。

1. avdt_l2c_appl


/* L2CAP callback function structure */
const tL2CAP_APPL_INFO avdt_l2c_appl = {avdt_l2c_connect_ind_cback,avdt_l2c_connect_cfm_cback,avdt_l2c_config_ind_cback,avdt_l2c_config_cfm_cback,avdt_l2c_disconnect_ind_cback,NULL,avdt_l2c_data_ind_cback,avdt_l2c_congestion_ind_cback,NULL,avdt_on_l2cap_error,NULL,NULL,NULL,NULL};
回调函数名触发时机说明所在连接阶段主要用途/作用描述
avdt_l2c_connect_ind_cback远端设备(如手机)发起连接请求时触发L2CAP 建立阶段接受连接、为新信令通道分配 SCB
avdt_l2c_connect_cfm_cback本地设备作为 发起方主动连接成功时 触发(L2CAP connect response)L2CAP 建立阶段通知连接成功、准备发送配置请求
avdt_l2c_config_ind_cback对方发送 L2CAP 配置请求(ConfigReq) 时触发L2CAP 配置阶段回应对方的配置请求
avdt_l2c_config_cfm_cback本地发送的 L2CAP 配置请求收到回应(ConfigRsp) 时触发L2CAP 配置阶段标记配置完成、准备发送 AVDTP 信令
avdt_l2c_disconnect_ind_cback信令通道或媒体通道 被远端断开或异常断开 时触发任意阶段(断链)清理资源、通知上层连接已断
NULL占位字段,无回调--
avdt_l2c_data_ind_cback收到远端 L2CAP 信道的数据包(AVDTP 信令包或媒体包)时触发正常通信阶段将数据交由 AVDTP 层解析(区分信令/媒体)
avdt_l2c_congestion_ind_cbackL2CAP 通道发生 拥塞状态变化(进入/退出拥塞) 时触发正常通信阶段通知上层暂停或恢复数据发送
NULL占位字段,无回调--
avdt_on_l2cap_errorL2CAP 通信过程出现异常(如参数错误、协商失败)时触发任意阶段(错误)异常处理、通知断链
其他 NULL预留未用字段(如 retransmit, QoS, flush, tx complete 等)--

8. 小结

看到这里, 我们已经 找到了 avdtp 关于 l2cap 层的回调了。 之后会有专门的篇幅来分析 车机主动连 a2dp, 被动连a2dp. 到时候, 就不用再梳理这块了。 本篇相当于是, 梳理一下整体思路。 方便之后的篇幅里面直接查找函数。

  1. 例如如果看到 bta_sys_sendmsg(p_buf):

    • 我们会第一时间去找 bta_av_hdl_event
  2. l2cap 中的回调,我们直接回去找 avdt_l2c_appl 变量中的函数即可。


/* L2CAP callback function structure */
const tL2CAP_APPL_INFO avdt_l2c_appl = {avdt_l2c_connect_ind_cback,avdt_l2c_connect_cfm_cback,avdt_l2c_config_ind_cback,avdt_l2c_config_cfm_cback,avdt_l2c_disconnect_ind_cback,NULL,avdt_l2c_data_ind_cback,avdt_l2c_congestion_ind_cback,NULL,avdt_on_l2cap_error,NULL,NULL,NULL,NULL};
  1. 在 avdt_ccb_ll_opened 函数中会调用 p_conn_cback, 从而触发 bta_ar_avdt_cback 的调用 -> p_av_conn_cback(实际调用的是 bta_av_conn_cback)
    1. avdt_ccb_ll_opened ->p_conn_cback[bta_ar_avdt_cback]
    2. bta_ar_avdt_cback->p_av_conn_cback[bta_av_conn_cback]

相关文章:

  • 个人mysql学习笔记
  • PubLayNet:文档布局分析领域的大规模数据集
  • WeakAuras Lua Script TOC BOSS2 <Lord Jaraxxus>
  • 学习设计模式《五》——工厂方法模式
  • 深度解析n8n全自动AI视频生成与发布工作流
  • 无人船 | 图解基于PID控制的路径跟踪算法(以欠驱动无人艇Otter为例)
  • 珈和科技助力“农险提效200%”!“遥感+”技术创新融合省级示范项目荣登《湖北卫视》!
  • 大数据利器:Kafka与Spark的深度探索
  • gem5 笔记01 gem5 基本应用流程
  • SpringBoot整合SSE,基于okhttp
  • 融山科技前端面经
  • 如何解决极狐GitLab 合并冲突?
  • 集结号海螺捕鱼游戏源码解析(第三篇):拉霸机模块开发详解与服务器开奖机制
  • 【Unity】简单实现基于TCP的阻塞式Socket的文本消息通信
  • 极狐GitLab 如何撤销变更?
  • git提交
  • [java八股文][Java基础面试篇]I/O
  • 缓存与数据库一致性方案
  • 二进制部署Kubernetes1.32.4最新版本高可用集群及附加组件
  • 最新扣子(Coze)案例教程:Excel数据生成统计图表,自动清洗数据+转换可视化图表+零代码,完全免费教程
  • 聚焦“共赢蓝色未来”,首届 “海洋命运共同体”上海论坛举行
  • 聚焦“共赢蓝色未来” “海洋命运共同体”上海论坛举行
  • 安徽临泉一小区交付后多楼层现裂缝,专家组论证称不影响安全
  • 从香料到文化,跟着陈晓卿寻味厦门
  • 国产手术机器人+5G技术,上海医生同一天远程为五地患者开刀
  • 江西九江市人大常委会原副主任戴晓慧主动交代问题,接受审查调查