Android源码第十三章:Bluetooth——沿一次发现走到控制器

做一个蓝牙设备搜索页,点下「开始发现」,startDiscovery() 已经返回 true,列表却还是空的。这个返回值告诉 App,服务接受了调用。页面上的设备,要等后续 ACTION_FOUND 广播带回来。在 Logcat 分开看 REQUEST、STARTED、FOUND 和 FINISHED,就能知道 App 在观察窗口中收到了哪些阶段的信号。

13.1 用 BluetoothAdapter 发现设备

BluetoothManager.getAdapter() 返回本机蓝牙的公开 SDK 入口。远端设备会由广播中的 BluetoothDevice 表示。这个示例在前台运行,要求 Android 13(API 33)及以上。先在系统设置开启蓝牙,并确认 Adapter 可用。Manifest 声明 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT,尚未授权时由 App 请求「附近设备」权限。

先登记接收器,再发起发现。下方只摘出调用顺序,附录包含空值检查、权限、结果读取和清理。

java 复制代码
BluetoothManager manager = getSystemService(BluetoothManager.class);
BluetoothAdapter adapter = manager.getAdapter();

IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
filter.addAction(BluetoothDevice.ACTION_FOUND);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);

boolean accepted = adapter.startDiscovery();

startDiscovery() 返回本次调用是否被接受。 ACTION_FOUND 的 EXTRA_DEVICE 带来设备对象,EXTRA_RSSI 可选。收到 ACTION_DISCOVERY_FINISHED 时关闭本地观察窗口。主动结束或离开页面时,示例只对已返回 true 的观察轮次尝试 cancelDiscovery(),然后注销 Receiver。发现是共享操作,系统是否会继续扫描要看 13.8 的条件。

接收器用 RECEIVER_EXPORTED 接收 Bluetooth 进程发送的受保护系统广播,只处理以上三个 action,并检查设备 extra。示例用 neverForLocation 声明不通过蓝牙结果推导物理位置,部分 BLE 信标可能被过滤。

发现、配对与连接

经典蓝牙 BR/EDR 用 Inquiry 寻找处于可发现模式的设备。低功耗蓝牙 LE 扫描接收广播,独立入口是 BluetoothLeScanner。本章跟踪的 startDiscovery() 在固定版本中还会在控制器支持 BLE 时请求 LE 扫描,所以 App 的 ACTION_FOUND 也可能来自 LE。要认定某条结果来自经典 Inquiry,还需 HCI 事件。

配对(pairing)协商认证与密钥。绑定(bonding)把配对关系保存下来。ACL 是承载上层协议的控制器链路。Profile 规定某类业务怎样互通,例如 A2DP 音频、HID 输入或 PAN 网络。GATT 通过服务、特征值和 ATT 组织数据。看到设备、绑定完成、ACL 建立、A2DP 连接和听见声音,各有各的证据,13.7 给出了查阅入口。

13.2 在 Android Studio 中观察一次发现

  1. 在 Android Studio 新建 Empty Views Activity,选 Java、包名 com.example.btstudy、Minimum SDK 33。保留模板的 Gradle 文件,确保安装了 app 模块 compileSdk 对应的平台,且 compileSdk 至少为 33。按[附录 A](#附录 A "#chapter13-demo")替换 Manifest 和 MainActivity,再同步 Gradle。
  2. 在 Device Manager 创建一台 Phone AVD,例如 Pixel 设备配置和 Android 14(API 34)Google APIs 镜像,选择适合电脑架构的系统镜像。启动后在 Android 设置检查蓝牙。选中该 AVD 并点击 Run,由 Android Studio 构建、安装和启动 App。
  3. 打开同一台 AVD 的 Logcat,用 package:com.example.btstudy tag:BtStudy 过滤。第一次点「开始一轮发现」时,如果系统询问,允许访问附近设备,再点一次发起发现。已经授权时点一次即可。保持 App 在前台,看界面或 Logcat。完成一轮后,可以再发起一次,并在观察窗口内点「结束本地观察并请求取消」。
看到什么 可以判断什么
NO_ADAPTER、BLUETOOTH_OFF 或权限提示 先检查镜像能力、系统蓝牙开关或附近设备授权。前置条件不满足时,本例尚未发起发现。
REQUEST accepted=true API 接受调用,后续可能启动新发现,也可能共享已有发现。
REQUEST accepted=false 或 REQUEST SecurityException 调用未返回受理,本例结束本地观察。权限、Adapter 状态和服务条件见 13.4。
STARTED、FOUND address=...、FINISHED 分别表示观察到开始、设备结果或发现结束广播。广播没有 App 专属请求 ID。结果可能来自其他应用也在使用的发现,结束广播也未说明结束原因。
LOCAL_END reason=... 本 App 已停止观察。若约 25 秒后仍未观察到结束广播,计时任务会尝试取消并退出。执行时间取决于主线程调度。

可以先关闭蓝牙,确认按钮显示 BLUETOOTH_OFF。再打开蓝牙,保持其他条件不变,观察是否走到 REQUEST。普通 AVD 用来观察 App 启动、授权、API 返回和实际收到的广播。当前镜像无可发现对端时,可能收到 FINISHED 而没有任何 FOUND。如需稳定复现经典发现结果,在一台 Android 13 及以上真机上运行同一个 App,准备一台由自己控制、已进入经典蓝牙可发现模式的设备。实验中的地址和设备名只用于本地对照。要证明发现结果来自经典 Inquiry,按附录 B 将 HCI 事件中的地址与该轮日志对上。

从 App 看向控制器

HCI(Host Controller Interface)定义 Host 协议栈与控制器之间的命令、事件和数据。JNI 在 Bluetooth 进程中把 Java 接到 Native 代码。默认 AIDL HAL 形成另一次 Binder 边界,负责把 HCI 包交给所选传输后端。

先看架构图的 Android 分层和运行位置。应用用 BluetoothAdapter 访问 Bluetooth 进程,system_server 管理服务与代理的生命周期。默认 HAL 可经 Linux HCI user channel 接驱动,也可回退到直接 fd。虚拟控制器是需要另行核对的端点。

流程图标出三个停点。一次调用可以被拒绝,可以复用既有发现,也可以生成新的 Inquiry。发现结果和结束广播随后进入应用观察窗口。

总时序选择一次空闲时发起、成功收到设备并自然结束的经典发现。图中沿同一组生命线展示本地投递、HCI 事件和应用主线程消费。控制器可能在 App 收到 FOUND 前就发出 Inquiry Complete,图示只是其中一种交错。提前取消在同图下半部分单独展开。

13.3 回看 Adapter、代理和 Native 回调怎样建立

回到图中的应用进程,先找 App 调用时已经持有的代理。应用取得的是 Context 创建的 BluetoothManager。其构造调用 BluetoothAdapter.createAdapter(mContext);getAdapter() 返回该字段。createAdapter 从 BluetoothFrameworkInitializer 的 service manager 取得 IBluetoothManager,再构造 Adapter。Adapter 构造登记进程级 manager callback,registerAdapter 返回的 Binder 被包装成 IBluetooth;服务上线/下线回调更新业务代理。管理者在 system_server,真正 startDiscovery 的接收者在 Bluetooth 进程,不是 BluetoothManagerService 代替协议栈搜索。

来源:packages/modules/Bluetooth/framework/java/android/bluetooth/BluetoothAdapter.java,行 1026~1032。BluetoothAdapter.java 固定源码。

java 复制代码
        mServiceLock.writeLock().lock();
        try {
            mService = registerBluetoothManagerCallback(mManagerCallback);
        } finally {
            mServiceLock.writeLock().unlock();
        }
    }

mService 保存当前服务代理;AttributionSource 保存调用归因信息。 远端设备不是从这个 Binder 参数里创建的;它由发现事件中的地址与 RemoteDevices 缓存建立。

Bluetooth 进程内,AdapterService 默认构造 new AdapterNativeInterface(),保存为 final 字段。onCreate 的初始化加载 bluetooth_jni 并调用 mNativeInterface.init(...);init 创建 AdapterNativeCallback,后续把 BondStateMachine 和 RemoteDevices 关联进去。JNI 注册时保存 &bluetoothInterface 函数表,initNative 对 NativeInterface 和它的 callback 做 GlobalRef。这些引用让 Native 可以在异步结果到来时回调同一组 Java 对象;并非把 Java 对象内存搬进控制器。 cleanupNative 在栈清理后删除全局引用,callback.cleanup 清空其业务关联,防止继续使用已结束服务。

来源:packages/modules/Bluetooth/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp,行 919~929。com_android_bluetooth_btservice_AdapterService.cpp 固定源码。

cpp 复制代码
  android_bluetooth_UidTraffic.clazz =
          (jclass)env->NewGlobalRef(env->FindClass("android/bluetooth/UidTraffic"));

  sJniAdapterServiceObj = env->NewGlobalRef(obj);
  sJniCallbacksObj = env->NewGlobalRef(env->GetObjectField(obj, sJniCallbacksField));

  if (!sBluetoothInterface) {
    return JNI_FALSE;
  }

JNI 的 obj 实际是 AdapterNativeInterface;历史变量名 sJniAdapterServiceObj 不应当作对象类型证据。属性回调写入 RemoteDevices 后,deviceFoundCallback 才用地址查出 BluetoothDevice 与 DeviceProperties。DeviceProperties 保存名称、RSSI、设备类型等信息,随后组装 Intent;应用收到的是 Parcelable 形式的设备身份和 extras,不是 Native property 指针。

13.4 发现请求从 Binder 进入搜索状态机

BluetoothAdapter.startDiscovery 检查 getState() == STATE_ON,通过 callServiceIfEnabled(s -> s.startDiscovery(mAttributionSource), false) 调用 AIDL;空代理和未开启返回 false。AdapterServiceBinder 的 startDiscovery 先用 getServiceAndEnforceCallerUserAndScan 检查可用服务、用户和扫描权限,再转到 AdapterService.startDiscovery。

服务还检查包名与 Binder UID 的归属、状态、位置相关条件,并把包名放进 mDiscoveringPackages。这是后面按包分发结果的依据;不是单纯调用 Native。若 ignoreRedundantDiscoveryIfSameState 生效且发现已在进行,重复同包可返回 false,新包可直接收到 STARTED 并共享既有发现;sendDiscoveredDevToNewPkgs 还控制缓存结果补发。即使返回 true,也可能复用已有发现,不能保证产生一条新的 Inquiry。 本文的底层主线针对空闲时的首次请求。

NativeInterface.startDiscovery 进入 startDiscoveryNative:

来源:packages/modules/Bluetooth/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp,行 983~992。com_android_bluetooth_btservice_AdapterService.cpp 固定源码。

cpp 复制代码
static jboolean startDiscoveryNative(JNIEnv* /* env */, jobject /* obj */) {
  log::verbose("");

  if (!sBluetoothInterface) {
    return JNI_FALSE;
  }

  int ret = sBluetoothInterface->start_discovery();
  return (ret == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
}

BT_STATUS_SUCCESS 被转换成 Java true。 接着看函数表指向的 Native 实现,才能知道"成功"承诺了什么。

这张函数表也有明确身份。JNI 注册函数 register_com_android_bluetooth_btservice_AdapterService 保存导出表地址:

来源:packages/modules/Bluetooth/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp,行 1998~1998。com_android_bluetooth_btservice_AdapterService.cpp 固定源码

cpp 复制代码
  sBluetoothInterface = &bluetoothInterface;

system/btif/src/bluetooth.cc 定义导出的 bt_interface_t bluetoothInterface,其中发现与取消字段分别绑定同文件的静态实现:

来源:packages/modules/Bluetooth/system/btif/src/bluetooth.cc,行 1235~1236。bluetooth.cc 固定源码

cpp 复制代码
        .start_discovery = start_discovery,
        .cancel_discovery = cancel_discovery,

因此 sBluetoothInterface->start_discovery() 调到下面的 start_discovery,不是根据同名函数猜出的路径。 指针选定本进程实现,真正异步调度要到它内部的 do_in_main_thread 才发生。

来源:packages/modules/Bluetooth/system/btif/src/bluetooth.cc,行 603~610。bluetooth.cc 固定源码。

cpp 复制代码
static int start_discovery(void) {
  if (!interface_ready()) {
    return BT_STATUS_NOT_READY;
  }

  do_in_main_thread(base::BindOnce(btif_dm_start_discovery));
  return BT_STATUS_SUCCESS;
}

这里先确认接口 ready,再向 Native main thread 投递 btif_dm_start_discovery。JNI 调用仍在进入它的线程上,真正的线程交接发生在 do_in_main_thread。Native 工作和 Binder 返回可以交错。示例在 App 主线程调用 SDK,并在同一主线程消费广播,因此本次调用的 REQUEST 日志会在本地接收器执行前写出。

如果把 API 改成同步等到所有发现结果,再一次性返回列表,调用方需要等完整扫描,还要处理超时、取消和其他应用共享发现的情况。当前入口无需等待发现完成,后续设备逐个通过广播到达。App 得以提前更新列表,同时要自己管理观察窗口,并接受广播没有请求 ID 的限制。

btif_dm_start_discovery 先检查已有排队搜索,初始化 inquiry 标记,再调用 BTA_DmSearch(btif_dm_search_devices_evt)。BTA_DmSearch 委托 bta_dm_disc_start_device_discovery,向搜索状态机送 BTA_DM_API_SEARCH_EVT;状态机的启动动作保存 callback,调用 BTM_StartInquiry。若 BTM 返回非 BTM_CMD_STARTED,BTA 走完成处理而非假装已开始。BTM 检查设备 up 与 inquiry 忙状态,登记 Inquiry 结果/完成事件 handler,保存结果和完成 callback,再构造命令。

来源:packages/modules/Bluetooth/system/stack/btm/btm_inq.cc,行 727~738。btm_inq.cc 固定源码。

cpp 复制代码
  // TODO: Register for the inquiry interface and use that
  bluetooth::shim::GetHciLayer()->EnqueueCommand(
          InquiryBuilder::Create(lap, btm_cb.btm_inq_vars.inqparms.duration, 0),
          get_main_thread()->BindOnce([](CommandStatusView status_view) {
            log::assert_that(status_view.IsValid(), "assert failed: status_view.IsValid()");
            auto status = status_view.GetStatus();
            if (status == ErrorCode::SUCCESS) {
              BTIF_dm_report_inquiry_status_change(tBTM_INQUIRY_STATE::BTM_INQUIRY_STARTED);
            } else {
              log::info("Inquiry failed to start status: {}", ErrorCodeText(status));
            }
          }));

InquiryBuilder::Create 携带 LAP、持续单位和响应数限制;0 表示不按响应个数提前结束。 HCI Inquiry 的时长单位为 1.28 秒,不能把字段直接读成毫秒。Command Status 成功后报告 inquiry started;它只说明控制器接受命令,后续结果事件与 Inquiry Complete 独立到来。本函数也调用 BTM_StartLeScan。所选 Android 实现会在控制器支持 BLE 时请求 LE 扫描,独立扫描器的过滤与回调另有一条链路。

看动图里 App 的返回值和控制器的命令状态怎样分别写入两个对象。

13.5 HCI 命令通过 HAL 到达控制器

HciLayer 和 HAL 之间传什么

HciLayer.EnqueueCommand 的 CommandStatusView 重载用 handler.CallOn 入队;send_next_command 在有 command credit、没有待回应命令且队列非空时,把 builder 序列化进 shared_ptr<vector<uint8_t>>,调用 hal_->sendHciCommand。命令队列保存对应 opcode 与 callback。其 credit 管理避免把任意数量命令直接塞给控制器;HAL 传输返回不等于收到 Command Status。

来源:packages/modules/Bluetooth/system/gd/hci/hci_layer.cc,行 365~372。hci_layer.cc 固定源码。

cpp 复制代码
      return;
    }
    std::shared_ptr<std::vector<uint8_t>> bytes = std::make_shared<std::vector<uint8_t>>();
    BitInserter bi(*bytes);
    command_queue_.front().command->Serialize(bi);
    hal_->sendHciCommand(*bytes);

    auto cmd_view = CommandView::Create(PacketView<kLittleEndian>(bytes));

HciHalImpl.sendHciCommand 先将发送字节交给 snoop logger,再调用 backend。HciHalImpl 构造先尝试 HciBackend.CreateAidl(instance),未声明 AIDL 时才 fallback HIDL。AIDL 分支使用服务名 android.hardware.bluetooth.IBluetoothHci/default(实例参数为空时),waitForService / fromBinder 得到代理,创建 AidlHciCallbacks 反向回调实体,再 initialize。AIDL/HIDL 是本版本选择分支,不应画成前后两层。 本文细追默认 AIDL 实现。

本版 hardware/interfaces 的默认服务在 service.cpp 创建 new BluetoothHci(),通过 Bluetooth offload 的 hal/ffi.h 转入 lib.rs::__add_bluetooth_hci_service 注册 Binder 服务。AIDL sendHciCommand 经 Rust HciProxy 的模块代理链、SinkModule 和 FFI 调用实现的 BluetoothHci::sendHciCommand;不能把新目录 bluetooth_hal 下另一个 HciProxyAidl 实现接到默认服务上。默认实现的 Send 给每包附上 H4 类型字节,经 writev 写 fd;事件由 AsyncFdWatcher 读取、H4 拆包,交给 hciEventReceived callback。

默认服务的跨语言桥梁

这条桥梁要同时跟住"哪个实现对象"与"哪些 HCI 字节"。先看服务创建入口,hci 指针来自刚创建的默认 BluetoothHci:

来源:hardware/interfaces/bluetooth/aidl/default/service.cpp,行 38~39。service.cpp 固定源码。

cpp 复制代码
    IBluetoothHci_addService(new BluetoothHci());
    ABinderProcess_joinThreadPool();

C++ 包装把实例指针存入 CInterface.handle,生命周期仍属于 HAL 服务;send_command 是回调函数指针,其 instance 就是这个 handle:

来源:packages/modules/Bluetooth/offload/hal/include/hal/ffi.h,行 119~121。ffi.h 固定源码。

cpp 复制代码
static inline void IBluetoothHci_addService(IBluetoothHci *hci) {
  __add_bluetooth_hci_service((CInterface){
          .handle = hci,

来源:packages/modules/Bluetooth/offload/hal/include/hal/ffi.h,行 128~132。ffi.h 固定源码。

cpp 复制代码
          .send_command =
                  [](void *instance, const uint8_t *data, size_t len) {
                    static_cast<IBluetoothHci *>(instance)->sendHciCommand(
                            std::vector<uint8_t>(data, data + len));
                  },

handle 确定调用哪个实现,data/len 确定交哪些字节;二者不是同一个地址。 Rust 注册函数将整个 CInterface 放进 Ffi,再把 HciProxy 包装成 Binder 服务:

来源:packages/modules/Bluetooth/offload/hal/lib.rs,行 68~76。lib.rs 固定源码。

rust 复制代码
    binder::add_service(
        &format!("{}/default", BpBluetoothHci::get_descriptor()),
        BnBluetoothHci::new_binder(
            HciProxy::new(vec![Box::new(LeAudioModuleBuilder {})], Ffi::new(cintf)),
            binder::BinderFeatures::default(),
        )
        .as_binder(),
    )
    .expect("Failed to register service");

HciProxy.initialize 构造 SinkModule 与模块代理链,登记 callbacks 的死亡监听后标记 Opened。AIDL sendHciCommand 要求处于 Opened,并调用 proxy.out_cmd;对普通 Inquiry,LeAudioModule 的默认命令分支透传给下一模块,不会把发现变成音频业务。

来源:packages/modules/Bluetooth/offload/hal/service.rs,行 114~121。service.rs 固定源码。

rust 复制代码
    fn sendHciCommand(&self, data: &[u8]) -> BinderResult<()> {
        let State::Opened { ref proxy, .. } = *self.state.read().unwrap() else {
            return Err(ExceptionCode::ILLEGAL_STATE.into());
        };

        proxy.out_cmd(data);
        Ok(())
    }

来源:packages/modules/Bluetooth/offload/hal/service.rs,行 162~164。service.rs 固定源码。

rust 复制代码
    fn out_cmd(&self, data: &[u8]) {
        self.ffi.send_command(data);
    }

末端 SinkModule 将同一片字节交给 Ffi;它持有 CInterface,在 Mutex 内用保存的 handle 和函数指针调回 C++:

来源:packages/modules/Bluetooth/offload/hal/ffi.rs,行 106~114。ffi.rs 固定源码。

rust 复制代码
    fn send_command(&self, data: &[u8]) {
        let intf = self.intf.lock().unwrap();

        // SAFETY: The C Code has initialized the `CInterface` with a valid
        //         function pointer and an initialized `handle`.
        unsafe {
            (intf.send_command)(intf.handle, data.as_ptr(), data.len());
        }
    }

调用关系为 AIDL → HciProxy → 模块代理链 → SinkModule → Ffi → CInterface.send_command(handle, data, len) → 原 BluetoothHci 实例。 这是函数委托与对象持有链,不凭跨语言边界假定新增线程。返回方向的 callback 也经包装回 AIDL;观察 H4 writev 与 fd watcher 时,调用对象已经明确。

分支一:Linux HCI user channel 与 USB

默认 initialize 先创建 NetBluetoothMgmt,尝试 openHci。该函数用 management 通道等待 HCI 设备,处理 rfkill,再创建 AF_BLUETOOTH/SOCK_RAW/BTPROTO_HCI socket,并绑定 HCI_CHANNEL_USER。这不是 HCI_CHANNEL_RAW:user channel 对设备有独占语义,绑定还受 CAP_NET_ADMIN、设备是否忙等条件限制。

来源:hardware/interfaces/bluetooth/aidl/default/net_bluetooth_mgmt.cpp,行 267~281。net_bluetooth_mgmt.cpp 固定源码。

cpp 复制代码
    // Open the raw HCI socket.
    int fd = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI);
    if (fd < 0) {
        ALOGE("unable to open raw bluetooth socket: %s", strerror(errno));
        return -1;
    }

    struct sockaddr_hci hci_addr = {
            .hci_family = AF_BLUETOOTH,
            .hci_dev = static_cast<uint16_t>(hci),
            .hci_channel = HCI_CHANNEL_USER,
    };

    // Bind the socket to the selected interface.
    if (bind(fd, (struct sockaddr*)&hci_addr, sizeof(hci_addr)) < 0) {

内核段以 kernel/common 提交 ccafb60de224d35a8bc14de33e1cd47caaaa786b 为例,覆盖 hci_sock.c、hci_core.c、btusb.c 和 hci_ldisc.c。正在运行的镜像和控制器需要各自的证据。

hci_sock_sendmsg 把首字节解释成 HCI 包类型,剥去类型字节,检查 user channel 可用包型后放入 hdev->raw_q,并 queue_work(tx_work)。hci_tx_work 取 raw_q,调用 hci_send_frame;后者调用注册在 hdev->send 的 Driver 回调。若设备由 btusb 驱动,其普通发送分支将 Command 包交给 control URB、ACL 交给 bulk URB;URB 是 USB 异步传输请求,不是蓝牙业务完成。

来源:本章取得的 Kernel 固定提交文件 btusb.c,行 2146~2155。

c 复制代码
	switch (hci_skb_pkt_type(skb)) {
	case HCI_COMMAND_PKT:
		urb = alloc_ctrl_urb(hdev, skb);
		if (IS_ERR(urb))
			return PTR_ERR(urb);

		hdev->stat.cmd_tx++;
		return submit_or_queue_tx_urb(hdev, urb);

	case HCI_ACLDATA_PKT:

返回方向是 USB interrupt URB 完成→btusb_recv_intr 重组事件→hci_recv_frame→rx_q/rx_work→hci_send_to_sock→user socket→HAL fd watcher。hci_rx_work 在 HCI_USER_CHANNEL 且非 HCI_INIT 时,把数据送 socket 后释放 skb 并跳过 Linux 普通事件/ACL 协议处理。Android 在用户态消费 HCI 事件;Linux 在这条路径提供传输与设备管理,而没有接管 Android 的 Profile。 USB 固件下载、厂商 setup 和专用 send 回调可能覆盖普通 btusb 分支,具体硬件须继续核对 VID/PID、模块、固件与绑定。

UART 也可能由 Kernel hci_uart 提供同类 HCI 设备:hdev->send 注册为 hci_uart_send_frame,协议 enqueue 后唤醒发送,TTY receive_buf 交给协议解析。这与下面 HAL 直接打开字符设备是两种安排,不应把同一包强制画成"直接 UART→Linux L2CAP→btusb"。

分支二:直接路径与虚拟控制器

openHci 失败后,默认 HAL 使用 vendor.ser.bt-uart 或默认 /dev/hvc5,open(O_RDWR),设置 raw terminal,并以 H4Protocol 收发。路径叫 hvc 并不能证明它是物理 UART;虚拟串口可承载相同 H4 数据。缺失节点、SELinux、服务实例与 Host 端点不匹配都可能使初始化失败。本版该初始化代码在错误 callback 后还有后续语句;不要据一条错误日志推断完善的短路收尾。

在可读的 netsim 分支,HciPacketTransport.Request 把包交给 AsyncManager,同步调度后调用 RootCanal 登记的 packet callback;响应经 Send→wireless.HandleResponse 回 transport dispatcher。RootCanal DualModeController.Inquiry 解析命令参数,调用 BrEdrController.Inquiry 并发 InquiryStatus;BrEdrController 保持 inquiry 状态、定时发送/接收模拟链路层 Inquiry,结果和超时变成 HCI 事件,取消清除 inquiry 状态。它模拟控制器,不替代 Android Host 栈。

来源:tools/rootcanal/model/controller/bredr_controller.cc,行 82~99。bredr_controller.cc 固定源码。

cpp 复制代码
ErrorCode BrEdrController::Inquiry(uint8_t lap, uint8_t inquiry_length, uint8_t num_responses) {
  if (num_responses > 0xff || inquiry_length < 0x1 || inquiry_length > 0x30) {
    return ErrorCode::INVALID_HCI_COMMAND_PARAMETERS;
  }

  if (inquiry_.has_value()) {
    INFO(id_, "Inquiry command is already pending");
    return ErrorCode::COMMAND_DISALLOWED;
  }

  auto now = std::chrono::steady_clock::now();
  inquiry_ = InquiryState{
          .lap = lap,
          .num_responses = num_responses,
          .next_inquiry_event = now + kInquiryInterval,
          .inquiry_timeout = now + std::chrono::milliseconds(inquiry_length * 1280),
  };

非法长度、已有 Inquiry 都能生成错误状态;没有模拟 peer 也可能正常完成且结果为空。 此处工具源码已固定提交可读;尚未把本机 Emulator 预编译 Host、启动参数、guest fd 与这一提交的 netsim/RootCanal 逐一对应。因此它是明确的候选虚拟后端,不冒充当前实例的事实。

实际辨认后端时,应同时取得:运行 fingerprint / uname、VINTF 和服务进程、HAL fd 指向的设备或 socket、sysfs Driver/总线信息、vendor.ser.bt-uart、Host 命令行与 netsim 日志。光看到 tools/netsim 目录、Profile 配置或 /dev/hvc5 的默认值,都不能确定本轮路径。

13.6 设备属性先入缓存,发现结果再广播

HCI 的 AIDL callback 到 AidlHciCallbacks.hciEventReceived;HciCallbacksImpl 先记录 snoop 并转交 HciLayer 的 hal_callbacks。它复制字节到 PacketView / EventView,用 handler.CallOn 投递 on_hci_event。此后按 event code 分派到此前登记、绑定 Native main handler 的 BTM 回调。接收 HAL callback 的线程与处理 BTM 状态的线程分开;不能把整条上行称为同一 Binder 线程。

BTM 的 on_incoming_hci_event 区分 Inquiry Complete、标准 Inquiry Result、RSSI 和 Extended 变体。结果被解析为 tBTM_INQ_RESULTS,经保存的 bta_dm_inq_results_cb 转为 tBTA_DM_SEARCH.inq_res,再触发 btif_dm_search_devices_evt 的 BTA_DM_INQ_RES_EVT。地址、RSSI、名称/EIR、设备类型被组织为 bt_property_t 数组。数组元素有 type、len、val;val 指向当时的地址/局部数据,所以异步交接前需要深拷贝。

来源:packages/modules/Bluetooth/system/btif/src/bluetooth.cc,行 1352~1361。bluetooth.cc 固定源码。

cpp 复制代码
void invoke_device_found_cb(int num_properties, bt_property_t* properties) {
  do_in_jni_thread(base::BindOnce(
          [](int num_properties, bt_property_t* properties) {
            HAL_CBACK(bt_hal_cbacks, device_found_cb, num_properties, properties);
            if (properties) {
              osi_free(properties);
            }
          },
          num_properties, property_deep_copy_array(num_properties, properties)));
}

property_deep_copy_array 在入 JNI 线程任务前复制属性。任务调用 callback 后用 osi_free 释放副本。 如果异步任务只保留原指针,生产者退出后,val 指向的局部数据可能已经失效。另一种做法是让任务持有一份有明确定义的共享所有权对象,这需要生产者和消费者共同约定生命周期。当前实现将副本交给任务,代价是每次交接都要分配和复制。 Native JNI 的 device_found_callback 从属性中找到地址,转成 jbyteArray;先调用 remote_device_properties_callback,再调用 Java deviceFoundCallback。JNI 局部引用用 ScopedLocalRef 随作用域归还,异步保活依赖前面登记的 GlobalRef。

来源:packages/modules/Bluetooth/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp,行 313~318。com_android_bluetooth_btservice_AdapterService.cpp 固定源码。

cpp 复制代码
  // Add device properties before announcing device found
  remote_device_properties_callback(BT_STATUS_SUCCESS, *addr, addr_type, num_properties,
                                    properties);

  sCallbackEnv->CallVoidMethod(sJniCallbacksObj, method_deviceFoundCallback, jaddr.get());
}

AdapterNativeCallback.kt 把属性更新交给 RemoteDevices。RemoteDevices 按地址缓存 BluetoothDevice / DeviceProperties,deviceFoundCallback 查缓存再调用 AdapterService.discoveryResultHandler;后者从属性构造 ACTION_FOUND,放 EXTRA_DEVICE、EXTRA_RSSI、EXTRA_NAME 等,遍历 mDiscoveringPackages,按权限和归因过滤定向给包。应用注册的 BroadcastReceiver 最终在它指定的接收调度环境执行;本例未指定 Handler,onReceive 在应用主线程。它不会直接得到 Native 栈回调线程。

先看独立副本交给谁,再看副本释放后哪份数据仍由 Java 保留。

同样的状态路径把 BT_DISCOVERY_STARTED/STOPPED 送给 AdapterProperties.discoveryStateChangeCallback。STOPPED 置 mNativeDiscovering=false、clearDiscoveryData、更新结束时间并发 ACTION_DISCOVERY_FINISHED。搜索自然完成可能还包含远端名称处理,不能把 Inquiry Complete 与 Java FINISHED 定义成同一个协议事件。

13.7 发现之后,配对、ACL 和 Profile 怎样接上

发现设备后若要普通配对,调用 device.createBond(),默认 TRANSPORT_AUTO;它经 IBluetooth.createBond→AdapterServiceBinder→AdapterService.createBond。服务检查已绑定/绑定中、双模与 feature flag 分支、enabled 和包名,登记发起者,取消发现,再向 BondStateMachine 发送 MESSAGE_CREATE_BOND。msg.obj 是 BluetoothDevice,arg1 是 transport。

BondStateMachine 默认与 AdapterService 同用 Bluetooth 进程主 Looper(默认构造传 Looper.getMainLooper),消费消息后查当前 bond state,把设备地址转 byte\[\]、取得地址类型,调用 NativeInterface.createBond。createBondNative 转 RawAddress;本版 LE 地址类型和 transport 会影响调用 create_bond_le 还是 create_bond。Native 函数先查 ready / pairing busy,再投递 btif_dm_create_bond;后续 BTA_DmBond 与安全管理分支协商密钥。API true、Java 已入队、Native 接受、认证完成是不同节点。 这里是配对主支路的接口解释,未逐包展开所有 SSP/SMP 或 CTKD。

Native bond_state_changed_callback 带 status、地址、transport、state、配对算法/变体与 HCI 原因回到 AdapterNativeCallback,BondStateMachine.bondStateChangeCallback 把 Native 状态转换为 BOND_BONDING/BONDED/NONE,封入 MESSAGE_BOND_STATE_CHANGE。状态机更新属性后发 ACTION_BOND_STATE_CHANGED;应用据新旧状态观察最终结果,PIN/SSP 用户确认交由系统 UI。失败必须结合 reason/status 解释,不能把回到 BOND_NONE 一概叫"取消"。

Profile 代理解决的是业务接口取得。BluetoothAdapter.getProfileProxy 按 PROFILE_CONSTRUCTORS 创建对应代理,登记 ProfileConnection,再 getProfile 得到 Binder;getProfileReply 调 connection.connect,之后通知 ServiceListener。它的 true 表示代理取得流程被接受,onServiceConnected 表示服务接口可用,不表示远端设备已连接。A2DP 的连接状态广播才描述该 Profile 的连接,真正媒体播放还需 Audio 路由与数据通道证据。本章不把音频数据交给 Inquiry 这条命令链,也不等待 Audio 章产物。

取得 Profile 代理后按接口查询连接状态,用完配对调用 closeProfileProxy;关闭代理是本地服务接口清理,不等价于断开设备音频连接。传统 A2DP 数据经 Native 音频/协议路径与 HCI ACL 或 offload 分支,LE Audio 可能涉及 ISO;具体传输与音频 HAL 在 Audio 场景继续确认。

13.8 取消请求、系统停止与本地退出

本地停止观察时,示例会尝试用 cancelDiscovery() 请求取消已受理的发现。同步返回与系统结束广播是两个观察点。若 ignoreRedundantDiscoveryIfSameState 生效,AdapterService 从 mDiscoveringPackages 移除调用包;还有其他包时可直接 true 而不下发全局取消。旧分支或最后一包才继续 Native cancel_discovery→主线程 btif_dm_cancel_discovery→BTA_DmSearchCancel→搜索状态机→BTM 取消/名称取消与最终完成。

BTM_CancelInquiry 在活跃时发送 InquiryCancel,处理其 Command Complete,并停止相关 LE 扫描;发现结束还要处理搜索状态、待处理请求和名称查询,不要求控制器在取消后补一条自然 Inquiry Complete。RootCanal 的 InquiryCancel 只在有活动 inquiry 时成功,清空状态并返回完成响应;动画和判据不能强行要求两种完成同时出现。

这里还有两条不能画成固定先后的路径。活跃 Inquiry 分支在排出取消命令后,直接继续通知并收尾搜索状态:

来源:packages/modules/Bluetooth/system/bta/dm/bta_dm_device_search.cc,行 130~135。bta_dm_device_search.cc 固定源码

cpp 复制代码
static void bta_dm_search_cancel() {
  if (BTM_IsInquiryActive()) {
    BTM_CancelInquiry();
    bta_dm_search_cancel_notify();
    bta_dm_search_cmpl();
  } else if (!bta_dm_search_cb.name_discover_done) {

BTM_CancelInquiry 不等待 HCI Command Complete;bta_dm_search_cmpl 可先经 BTA_DM_DISC_CMPL_EVT 上报 STOPPED。 HCI 取消回应后来又可经 btm_process_cancel_complete 上报 STOPPED,fix_multiple_discovery_stopped_broadcast 控制重复状态过滤。Java FINISHED、应用 LOCAL_END 和控制器取消回应不是同一个终点,不能据示意排版推出全局先后保证。

在图中比较应用自己的 Receiver 关闭与系统发现继续这两个结果。

收到 FINISHED 广播时,示例清理 Receiver 和计时任务。用户停止、离开前台或约 25 秒后仍未观察到结束时,已受理的轮次还会尝试取消。请求返回 false 或抛出异常只清理本地,避免把拒绝的请求变成对既有发现的取消。系统端是否已经登记、搜索何时停止,需要结合服务状态与 HCI 日志判断。

现象 先查哪里 不能直接推出什么
startDiscovery false / SecurityException 蓝牙状态、SCAN/CONNECT、用户/包归因、重复请求 控制器损坏
true 后无 STARTED 日志观察器、排队、复用分支、Native/HCI 状态 一定没发 HCI
FINISHED 且无 FOUND peer 可发现模式、BR/EDR 能力、距离/虚拟 peer、过滤条件 本轮发现实现失败
snoop 有结果,App 没结果 属性解析、权限过滤、包登记、receiver 窗口 无线控制器没收到
cancel true 后系统仍在发现 其他扫描包、flag、在途事件和名称查询 取消已全局完成
HAL 初始化失败或死亡 VINTF/实例、fd/SELinux、user channel busy、Host 端点 只重试 App 即可恢复
bond 回 NONE status、reason、用户确认与 peer 状态 只是用户取消

正常服务结束时,Native 清理栈、JNI GlobalRef 与 callback 关联;HciHalImpl 析构 reset backend,AidlHci 析构解除 death recipient 并调用 HAL.close。默认 HAL.close 停 fd watcher,关闭 user channel 或直接 fd,清空 H4、恢复 READY;clientDied 也走 close。应用进程退出使动态 receiver 随进程结束,但这不能替代示例在正常 onStop 的显式清理,也不能证明控制器瞬时停止。HAL 死亡在本版 AIDL 客户端有致命恢复策略,不应描述为透明无损重连。

13.9 按日志回查发现进度

先在 Logcat 确认 REQUEST,再看 STARTED、FOUND、FINISHED 与 LOCAL_END。需要追到系统内部时,从 AdapterService 的包登记出发,沿 Native 调度、HCI 命令和 HAL 的所选传输走到控制器,再从事件解析、属性复制、Java 缓存回到应用广播。

按问题查符号:入口与代理看 BluetoothManager / BluetoothAdapter;归因与分发看 AdapterServiceBinder / AdapterService;异步接受看 bluetooth.cc::start_discovery;搜索状态看 bta_dm_device_search.cc;Inquiry 打包与事件看 btm_inq.cc;HCI 队列看 hci_layer.cc;传输选择看 hci_hal_impl_android.cc / hci_backend_aidl.cc / 默认 BluetoothHci.cpp;回调与释放看 invoke_device_found_cb / AdapterNativeCallback.kt / RemoteDevices。

  1. startDiscovery true 能证明已经找到设备吗?答:不能,13.4 的返回不等待异步任务完成,不能证明控制器已接受或已发现设备,也可能共享既有发现。
  2. HCI user channel 是否让 Android 使用 Linux L2CAP?答:不能这样推导,13.5 的 user channel 接收分支跳过普通 Linux 协议处理,Android 栈仍消费事件。
  3. 为什么属性需要深拷贝?答:bt_property_t.val 可能指向当前函数局部数据,13.6 的异步任务必须延长其数据生命期。
  4. FINISHED 且零设备是否失败?答:不一定;先以受控 peer 与后端能力验证前提,见 13.2、13.8。
  5. getProfileProxy 的 onServiceConnected 是否表示耳机已连接?答:仅服务接口可用,Profile 连接与音频输出需独立证据,见 13.7。
  6. 超时后取消 true,能否据此判断控制器已经停下?答:还要对照同轮 HCI 取消回应及后端日志,见附录 B。

附录 A|可复制的发现示例

按 13.2 创建项目后覆盖下面两个文件。模板的 namespace 和 applicationId 使用 com.example.btstudy。界面由 Activity 创建,无需另装依赖或添加布局资源。

app/src/main/AndroidManifest.xml

xml 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-feature android:name="android.hardware.bluetooth" android:required="true" />
    <application android:label="Bluetooth 一轮发现" android:theme="@android:style/Theme.Material.Light.NoActionBar">
        <activity android:name=".MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

app/src/main/java/com/example/btstudy/MainActivity.java

java 复制代码
package com.example.btstudy;

import android.Manifest;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Insets;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.ScrollView;

public final class MainActivity extends Activity {
    private final Handler handler = new Handler(Looper.getMainLooper());
    private BluetoothAdapter adapter;
    private TextView status;
    private boolean active, registered, discoveryAccepted;
    private int round;
    private Runnable timeout;

    private void event(String text) {
        String line = "round=" + round + " elapsedMs=" + SystemClock.elapsedRealtime()
                + " " + text;
        Log.i("BtStudy", line);
        status.append(line + "\n");
    }

    private boolean permissionsReady() {
        return checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN)
                == PackageManager.PERMISSION_GRANTED
                && checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT)
                == PackageManager.PERMISSION_GRANTED;
    }

    private final BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override public void onReceive(Context context, Intent intent) {
            if (!active) return;
            String action = intent.getAction();
            if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
                event("STARTED");
            } else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                BluetoothDevice device = intent.getParcelableExtra(
                        BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
                if (device != null) {
                    String rssi = intent.hasExtra(BluetoothDevice.EXTRA_RSSI)
                            ? Short.toString(intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, (short) 0))
                            : "unknown";
                    String name = intent.getStringExtra(BluetoothDevice.EXTRA_NAME);
                    event("FOUND address=" + device.getAddress()
                            + " name=" + name + " rssiDbm=" + rssi);
                }
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
                event("FINISHED");
                end("finished-broadcast", false);
            }
        }
    };

    @Override public void onCreate(Bundle state) {
        super.onCreate(state);
        BluetoothManager manager = getSystemService(BluetoothManager.class);
        adapter = manager == null ? null : manager.getAdapter();
        LinearLayout box = new LinearLayout(this);
        box.setOrientation(LinearLayout.VERTICAL);
        int padding = Math.round(12 * getResources().getDisplayMetrics().density);
        box.setOnApplyWindowInsetsListener((view, insets) -> {
            Insets bars = insets.getInsets(WindowInsets.Type.systemBars());
            view.setPadding(bars.left + padding, bars.top + padding,
                    bars.right + padding, bars.bottom + padding);
            return insets;
        });
        status = new TextView(this);
        Button start = new Button(this);
        start.setText("开始一轮发现 / 请求权限");
        start.setOnClickListener(v -> begin());
        Button stop = new Button(this);
        stop.setText("结束本地观察并请求取消");
        stop.setOnClickListener(v -> end("user-stop", true));
        box.addView(start);
        box.addView(stop);
        ScrollView logView = new ScrollView(this);
        logView.addView(status);
        box.addView(logView, new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, 0, 1));
        setContentView(box);
        event("READY 在设置开启蓝牙,然后点击开始");
    }

    private void begin() {
        if (active) { event("ALREADY_ACTIVE"); return; }
        if (!permissionsReady()) {
            requestPermissions(new String[] {Manifest.permission.BLUETOOTH_SCAN,
                    Manifest.permission.BLUETOOTH_CONNECT}, 1);
            event("授权后再次点开始;拒绝则不发请求");
            return;
        }
        if (adapter == null) {
            event("NO_ADAPTER 当前镜像未提供蓝牙 Adapter");
            return;
        }
        if (!adapter.isEnabled()) {
            event("BLUETOOTH_OFF 请在设置打开蓝牙");
            return;
        }
        round++;
        status.setText("");
        IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
        filter.addAction(BluetoothDevice.ACTION_FOUND);
        try {
            registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
            registered = true;
            active = true;
            discoveryAccepted = adapter.startDiscovery();
            event("REQUEST accepted=" + discoveryAccepted);
            if (!discoveryAccepted) {
                end("request-rejected", false);
                return;
            }
            final int expectedRound = round;
            timeout = () -> {
                if (active && round == expectedRound) end("timeout-25s", true);
            };
            handler.postDelayed(timeout, 25_000);
        } catch (SecurityException e) {
            event("REQUEST SecurityException");
            end("permission-error", false);
        }
    }

    private void end(String reason, boolean cancel) {
        if (!active && !registered) return;
        active = false;
        if (timeout != null) handler.removeCallbacks(timeout);
        timeout = null;
        if (cancel && discoveryAccepted && permissionsReady()) {
            try { event("CANCEL accepted=" + adapter.cancelDiscovery()); }
            catch (SecurityException e) { event("CANCEL SecurityException"); }
        }
        discoveryAccepted = false;
        if (registered) { unregisterReceiver(receiver); registered = false; }
        event("LOCAL_END reason=" + reason);
    }

    @Override protected void onStop() {
        end("onStop", true);
        super.onStop();
    }
}

附录 B|对照应用日志与 HCI

13.2 的 App 可以照常由 Android Studio 安装到受控设备。打开开发者选项的 Bluetooth HCI snoop log,按测试机要求重启蓝牙。先在终端 A 设置实际序列号并保存运行基线。另一个终端须独立设置同一序列号。

bash 复制代码
export SERIAL=your-test-serial
RUN_DIR=$(mktemp -d) && (
    set -e
    adb -s "$SERIAL" shell getprop ro.build.fingerprint > "$RUN_DIR/fingerprint.txt"
    adb -s "$SERIAL" shell dumpsys bluetooth_manager > "$RUN_DIR/bluetooth-before.txt"
    printf '%s\n' "$RUN_DIR"
)

在终端 B 打开 Logcat 采集。把路径换成终端 A 实际打印的目录,确认两端设备号一致。保持观察器运行,在 App 上发起一轮,等待结束或主动停止。

bash 复制代码
export SERIAL=your-test-serial
RUN_DIR='/replace/with/printed/path'
(
    set -o pipefail
    test -d "$RUN_DIR" || exit 1
    adb -s "$SERIAL" logcat -v threadtime BtStudy:I '*:S' 2>&1 | tee "$RUN_DIR/app.log"
)

结束后在终端 A 采集系统状态和 bugreport。

bash 复制代码
test -d "$RUN_DIR" && (
    set -e
    adb -s "$SERIAL" shell dumpsys bluetooth_manager > "$RUN_DIR/bluetooth-after.txt"
    adb -s "$SERIAL" bugreport "$RUN_DIR/bluetooth-bugreport.zip"
    test -s "$RUN_DIR/bluetooth-bugreport.zip"
)

在归档中找到本轮 btsnoop 文件,再在 Wireshark 查 Inquiry(opcode 0x0401)、Command Status、Inquiry Result 系列与 Inquiry Complete。取消路径另查 Inquiry Cancel(0x0402)及 Command Complete。若归档中没有本轮 snoop,就保留 App 侧结论,按产品支持的采集入口继续定位。

elapsedMs 来自开机后的单调时间,包含休眠。Logcat 默认的日期时间是另一套时钟。先用同一轮 REQUEST 和 LOCAL_END 标出 App 观察窗口,再结合事件地址、操作时间和状态核对 HCI。Inquiry 可能早于 REQUEST,取消回应可能晚于 LOCAL_END,因此采集应从点击前开始,并延续到观察到系统结束或达到另设的采集时限。停掉终端 B 的采集,恢复 HCI snoop 设置,并按产品要求重启蓝牙。以上为跟做步骤,当前没有对应的设备运行记录。

源码版本

项目 固定提交 讲解范围
packages/modules/Bluetooth c77db469de80f86660bc053bec4dee0c5d4b947c SDK、Adapter 服务、BTIF/BTA/BTM、HCI、HAL FFI
hardware/interfaces 0162af698935100a590b7359581ac8b1b80693e5 Bluetooth 默认 AIDL HAL
tools/rootcanal 559446380c3e2cf01c82608897ba5a246996fe5c 虚拟控制器与 Inquiry
tools/netsim 71feaeea338b3bd7eda6914cc9b9b382d752957d 虚拟 HCI 包传输
device/generic/goldfish 296e55aa0244e8929e393e00e34471fef2a5d662 Emulator 产品配置线索
kernel/common ccafb60de224d35a8bc14de33e1cd47caaaa786b HCI socket、USB 与 TTY 驱动

参考资料

相关推荐
booboosui1 小时前
派活给另一个 AI:前后两次文件夹快照,就是交付清单
android
普通人66 小时前
MySQL_FIND_IN_SET函数详解
android·数据库·mysql
好的佩奇18 小时前
Dart 之集合元素
android·flutter·dart
Easyhood19 小时前
Android 开发入门实践(十四):Compose 副作用入门与四个常用 API
android·开发语言·kotlin·android jetpack·compose
FlightYe20 小时前
音视频修炼之容器与协议(四):RTSP详解
android·linux·ffmpeg·音视频·dash
疯华整猫21 小时前
删掉 LeakCanary 那天,我用 Trae 自己撸了一套:最难的不是分析堆,是决定什么时候 dump
android·人工智能·性能优化
mmsx1 天前
耳切法切带洞多边形:论文能跑通,真实数据为什么崩
android·kotlin
洞见前行1 天前
你的 Activity 关了,为什么还活着?Android 内存泄漏从发现、定位到线上治理
android·性能优化·leakcanary