苹果 AirPods 协议,Android 也可以使用完整版 AirPods 能力

最近刚好在想,怎么在 Android 上接入 AirPods 的全部能力,刚好就看到了 librepods这个项目,它是一个能让 Android 使用 AirPods 的专属功能的开源项目,比如:

在非 Apple 平台上支持更改降噪模式、快速入耳检测、精确电池状态、对话感知这些能力。

librepods 原理是通过逆向,还原了 Apple 私有的 AirPods 通信协议(AACP,Apple Accessory Communication Protocol),然后在 Android/Linux 上用标准蓝牙 API 重新实现了这套协议:

  • BLE 广播层,被动读取设备状态
  • L2CAP (PSM 0x1001/4097) , AACP 控制信道,双向命令通信
  • ATT (PSM 31) , GATT 属性层,读写特定特性(透明度/降噪/听力辅助)

首先是 BLE 广播解析,因为 AirPods 会持续广播 BLE 数据包,manufacturer ID 为 76(即 Apple 的 company ID),所以项目里 BLEManager.kt 会针对这些数据包进行扫描和解析,从中读取:

  • 型号识别(model ID,如 0x2420 对应 AirPods Pro 2 USB-C)
  • 电量(左右耳机和充电盒,4 bit 一组)
  • 入耳状态(status 字节的位运算)
  • 充电盒盖开关状态
  • 连接状态

这一层是被动的,只要扫描到蓝牙广播包即可解析,同时为了安全性,AirPods 较新固件的 BLE 广播数据做了加密(最后 16 字节用 AES-ECB 加密),解密密钥(ENC_KEY)和身份解析密钥(IRK)需要通过 AACP 连接后主动向设备请求 proximity keys 获取,然后存储在本地,这就是为什么 BLE 功能需要先建立一次 AACP 连接。

然后就是 L2CAP 通道上的 AACP 控制协议,这也是整个项目最核心的部分 ,AirPods 在标准蓝牙协议栈之上开放了一个私有的 L2CAP 通道,PSM(Protocol Service Multiplexer)为 0x1001(4097),对应在 AACPManager.kt 代码完整实现了这套协议,协议里,所有 AACP 包头固定为:

复制代码
04 00 04 00

之后跟两字节小端 opcode,然后是数据,然后握手包是连接后必须发送的第一个包,如果没有的话 AirPods 不会响应任何后续命令:

复制代码
00 00 04 00 01 00 02 00 00 00 00 00 00 00 00 00

握手后需要发送 feature flags 包(opcode 0x4D)来解锁对话感知(Conversational Awareness)和自适应透明度等功能,再发送 notification request 包(opcode 0x0F)订阅来自 AirPods 的主动通知(电量、入耳、噪音模式等)。

控制命令格式统一为 opcode 0x09,跟随 identifier 字节和数据字节:

css 复制代码
04 00 04 00 09 00 [identifier] [data1] [data2] [data3] [data4]

目前已逆向出 60 余个控制命令,覆盖:降噪模式切换、对话感知开关、按键配置、自动连接、单耳 ANC、入耳检测开关、听力辅助开关等场景。

AirPods 的响应是对称的,发什么格式就回什么格式,状态变更也用同一套包结构主动推送。

然后就是 ATT 层(GATT over L2CAP),通过 PSM 31 建立另一个 L2CAP 连接。其实就是裸 ATT 协议,绕过了 GATT 的 UUID 层,实现在 ATTManager.kt 代码,主要用来读写下面放特性:

  • ATTHandles.TRANSPARENCY // handle 0x18
  • ATTHandles.LOUD_SOUND_REDUCTION // handle 0x1B
  • ATTHandles.HEARING_AID // handle 0x2A

其实透明度模式的精细参数(EQ、放大、音调、对话增益等)和听力辅助参数(听力图)就通过这个通道读写。

然后就是 Android 上的适配,这部分其实还是有点难度,因为 Android 蓝牙栈的 L2CAP 限制,标准 Android 蓝牙栈(Fluoride/Gabeldorsche)对经典蓝牙 L2CAP 连接的 FCR(Flow Control and Retransmission)模式,协商存在一个 bug ,在大多数设备上不能直接建立到 PSM 0x1001 的 L2CAP 通道,这其实是所有问题的起点

然后目前项目用了两个解法:

Xposed + 原生 Hook
  • KotlinModule.kt 是一个 libxposed 模块,在蓝牙应用进程(com.google.android.bluetoothcom.android.bluetooth)加载时,会注入一个名为 libl2c_fcr_hook.so 的原生共享库,这个库 hook 了蓝牙协议栈底层的 l2c_fcr_chk_chan_modes 函数,修改了它对 FCR 模式检查的行为,从而让底层的 L2CAP 连接能够成功建立。
  • BluetoothConnectionManager.kt 通过反射调用 BluetoothSocket 的私有构造函数,直接指定 socket 类型为 L2CAP(type=3)和 PSM 值,绕过了 Android 公开 API 的限制:
go 复制代码
val type = 3 // L2CAP
arrayOf(adapter, device, type, true, true, psm, uuid) // 多种签名尝试

由于不同 Android 版本的 BluetoothSocket 内部构造函数签名不同,代码枚举了 5 种参数组合逐一尝试。

VendorID 欺骗

由于部分功能(多设备连接切换、ATT 特性访问、听力辅助等)被 AirPods 锁定,只向 Apple 设备开放,所以 AirPods 通过蓝牙的 DID Profile(Device ID Profile)检查连接设备的 VendorID,Apple 的 VendorID 为 0x004C

所以只要通过将 Android 设备的蓝牙 DID Profile 的 VendorID 改为 0x004C,AirPods 会把设备识别为 Apple 设备并解锁这些功能,在 Android 上这需要通过 Xposed hook 蓝牙服务来修改,在 Linux 上是在 /etc/bluetooth/main.conf 中添加:

ini 复制代码
DeviceID = bluetooth:004C:0000:0000

外,在 ColorOS/OxygenOS 16、Realme UI 7.0 以及 Pixel 的 Android 16 QPR3 ,目前已经修复了上面蓝牙栈 bug ,也有提供合规的 L2CAP 支持,在这些设备上不需要 Xposed 支持 AACP 连接。

另外还有一个重要功能就是多设备切换(Smart Routing) ,AirPods 支持同时连接两台设备,设备间通过 AACP 的 Smart Routing 机制(opcode 0x10/0x11)协商谁拥有连接控制权。

librepods 里也完整实现了这套协议,createMediaInformationPacketcreateHijackRequestPacket 分别对应"通知对方我在播放 "和"主动抢占连接 "两个场景,数据包内甚至包含类似 JSON key-value 格式的字段(PlayingAppHostStreamingStatebtName: Android 等)。

目前看起来,作者主要是通过 macOS 设备的 PacketLogger 抓取蓝牙流量逆向的 AirPods 机制,不过空间音频(头部追踪 HRTF)需要系统级音频集成,这个能力还是暂时确实,而且心率监测(AirPods Pro 3 及以后)协议也还没逆向完成,不过能让 AirPods 多发挥点场景适配还是挺不错的。

不过最让我没想到的是,原来 Android 上的 L2CAP bug 居然是常见的,我一直以为只是小部分厂商问题。

链接

github.com/kavishdevar...

相关推荐
IT_陈寒1 小时前
JavaScript的默认参数挖坑实录,我掉进去了
前端·人工智能·后端
黄林晴1 小时前
告别无效重建:Gradle 9.6.0 解决 CI 构建缓存失效痛点告别无效重建:Gradle 9.6.0 解决 CI 建筑缓存失效痛点
android·gradle
张风捷特烈2 小时前
Flutter 类库大揭秘#01 | path_provider架构与设计
android·flutter
_阿南_11 小时前
Android文件读写和分享总结
android
kyriewen13 小时前
别再 console.log 了:5 个 Chrome DevTools 调试技巧,用过就回不去了
前端·javascript·面试
IT_陈寒15 小时前
Python搞不定字符串编码?这破玩意坑我两小时!
前端·人工智能·后端
DigitalOcean16 小时前
Laravel 开发者已在 DigitalOcean 上开通超过 10 万台服务器
前端·laravel
星始流年17 小时前
从 Tool 到 Skill——基于 LangChain 的服务端Skill实现
前端·langchain·agent
李惟17 小时前
开源本地通信库,纯客户端 RPC,像聊天一样通信
前端