让自编译的纯 AOSP 15 打通中国移动 VoLTE 电话(Pixel 9 Pro XL)

前言

手头一台 Pixel 9 Pro XL(代号 komodo),想刷一个自己从源码编译的、尽量纯净的 AOSP 15当日用机。其他都好说——双击熄屏、root、蓝牙、5G 数据、电池百分比,一个个都搞定了。

唯独有一件事,几乎所有教程和论坛都告诉你”做不到”:

自编译的纯 AOSP 在国内打不了电话

中国移动卡,插上去,数据 5G 满格,但一拨号——秒断

这篇就是记录怎么把这块硬骨头啃下来的全过程。剧透:最后在 SELinux enforcing 模式下,自编译纯 AOSP 跑通了完整的双向中国移动 VoLTE 通话(AMR-WB,上下行都有声,通话稳定不掉线)。中间踩的坑可以说是一层套一层。

注:本文所有设备序列号、手机号、IMSI/ICCID、签名密钥指纹、内网地址等已脱敏。保留的 MCCMNC 46000carrierid 1435 是中国移动的公开标识。


一、为什么打不了?先搞清楚根因

国行 Pixel 不存在这个问题,海外版才有。逐层排查后,根因有两道墙:

第一道墙:Google 在非国行 Pixel 上默认关掉了中国移动的 VoLTE。

中国移动是 4G/5G 纯 IMS 语音,没有 2G/3G 的 CS 电路域回落。也就是说,语音只能走 VoLTE。而 Google 官方的运营商配置里:

1
carrier_volte_available_bool = false   // 对中国移动默认关闭

VoLTE 一关,又没有 CS 回落,结果就是拨号即断

第二道墙:运营商配置本身在国内拉不下来。

Pixel 的运营商配置不是 AOSP 默认的 com.android.carrierconfig 提供的,而是闭源的 CarrierSettings(包名 com.google.android.carrier)。这玩意儿要连 Google 服务器做 checkin 才能下发对应运营商的配置——而这个 checkin 在国内被墙,拉不到,配置版本号是空的。

所以两件事叠加:配置下不来,就算下来了 VoLTE 也是关的。


二、先在原厂固件上验证可行性

在动手改 AOSP 之前,先证明这条路走得通——设备、SIM、基带本身有没有能力打中国移动 VoLTE?

在官方固件上做了两件事:

  1. 挂一个能连 Google 的代理。 设备一旦能连上 Google,CarrierSettings 立刻 checkin 成功,下发中国移动配置(版本号变成 chinamobile_cn-...,本地缓存出现对应的 carrierconfig-*-1435.xml)。
  2. root + Pixel-IMS 强开 VoLTE。 Pixel 9 用 Magisk root(注意 Pixel 9 要patch 的是 init_boot.img,不是 boot.img),再用 Pixel-IMSdev.bluehouse.enablevolte)经 Shizuku 把 carrier_volte_available_bool 强行改成 true

小坑:cmd phone cc set-value 即使有 root 也会 “Permission denied”,所以得用 Pixel-IMS 这种走载体特权的 app,而不是命令行。

结果:官方固件上中国移动通话拨通,AMR-WB(codecType=2,16kHz 宽带语音)over VoLTE,无报错。

这就证明了:硬件和 modem 固件完全有能力,问题 100% 在 Android 软件层。 Google 只是默认配置把它关了。这给了我啃自编译版本的信心。


三、真正的目标:让自编译 AOSP 也能打

原厂能打不算本事。目标是:从 AOSP 源码编译出来的 ROM,自己也能打通中国移动电话。

我给自己定的原则是:除了 telephony 这一块允许引入闭源的 Google/三星组件,其余一切保持最纯的 AOSP。 毕竟电话栈(基带交互、IMS、媒体编解码)这种东西,指望纯开源实现去对接真实运营商核心网,是不现实的。

死路一条:手搓移植电话栈

最初的想法很朴素:把官方固件里跟电话相关的几个 APK/JAR 一个个挑出来,塞进 AOSP 源码树,重签平台 key。

结果撞上了经典报错:

1
1610 CODE_REJECT_UNSUPPORTED_SDP_HEADERS

通话建立时,Shannon 基带生成的 SIP INVITE 里的 SDP 被中国移动核心网拒绝。

折腾了很久才想明白根因:手工挑选的电话栈是”不连贯”的。 整套 Pixel 电话栈是一个互相咬合的有机整体——

  • Google RIL 主干:grilservice + google-ril.jar + RilConfigService
  • IMS:ShannonIms(com.shannon.imsservice
  • 媒体:PixelImsMediaService + 原生库 libpixelimsmedia.so
  • 一堆 ril-extensionOemRilService、sitril 原生库……

手工挑,必然漏东西(尤其是那些藏在 lib/ 里的 .so 原生库),而且很容易混进版本不匹配的旧 blob。栈不连贯 → 生成的 SDP 不对 → 1610。

热补这条路验证了假设就果断放弃,没有在错误的方向上浪费几小时的编译时间。

转折点:adevtool 连贯提取

关键工具是 adevtool——GrapheneOS 用来从官方固件完整、连贯地提取专有 blob 的工具。它的价值在于:它知道整套电话栈的依赖关系,一次性把匹配同一套框架 ABI 的所有组件(包括原生库、sepolicy、各种 .pb 配置)成套提取出来,而不是让你手工去猜哪个该拿哪个不该拿。

做法:

  1. vendor/adevtool 克隆 GrapheneOS 版本,把版本对齐到我编译的 base(BP1Aandroid-15.0.0_r36)。
  2. adevtool 的 extract,成套拉出 grilservice、google-ril.jar、PixelImsMediaService(libpixelimsmedia.so / sitril 原生库一起)、ShannonIms,以及 427 个 CarrierSettings 的 .pb 配置文件

这一步直接解决了 1610——栈连贯了,SDP 就对了。

但真正的折磨,从能编译出镜像之后才开始。


四、编出来了,但开不了机:连环 boot 坑

集成进源码树、m 编译通过、刷进设备——卡在 Google logo。 然后是连续好几个晚上的 bringup 排查。

坑 1:根本没生成 vendor.img

adevtool 生成的 BoardConfigVendor.mk 没有被安装(只躺在一个 .skip 文件里),而且 device-vendor.mk 被截断了,缺了 VINTF manifest 和 200 多个 HAL 包的声明。结果就是 vendor.img 压根没编出来。

修法:从 .skip 恢复 BoardConfigVendor.mk(它设置了 BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := ext4),从骨架恢复被截断的 device-vendor.mk

教训:用自己的 system + 官方的 vendor 会因为 VINTF/sepolicy 不匹配而 boot 失败。 必须有一套自己编出来的、连贯的 vendor.img。

坑 2:7 个 vendor HAL 二进制 SELinux 标签错了

komodo 的 product 配置没有引入 gs-common 那套 per-HAL 的 sepolicy,导致 7 个 vendor HAL 可执行文件的 file_contexts 漏标,全部落到了通用的 vendor_file 类型上。后果是 init 无法对它们做域转换(domain transition),开机就卡在音频 HAL 启动那一步。

最关键的一条:

1
/vendor/bin/hw/android.hardware.audio.service-aidl.aoc   u:object_r:hal_audio_default_exec:s0

补上正确的 file_contexts 标签后,HAL 才能被拉起来。

坑 3:service_contexts 缺了音频扩展服务 → HAL 直接 abort

补完上面那条,enforcing 下又陷入 boot loop。日志里:

1
2
avc: denied { add } ... vendor.google.whitechapel.audio.extension.IAudioExtension ...
tcontext=u:object_r:default_android_service:s0

音频 HAL 启动时要往 servicemanager 注册 IAudioExtension 服务,但 service_contexts 里没给这个服务定义类型,注册被拒,HAL 直接 SIGABRT 崩溃,于是 boot loop。

修法:补上 hal_audio_ext_service 类型 + 对应的 service_contexts 条目。后来发现引入 gs-commonaidl.mk 之后它会原样提供这些定义,就把手动加的删掉去重了(见下文)。

坑 4:刷机姿势——verity 与 super 不一致

中途为了省时间只单独 resize 重刷了 vendor 分区,结果 super 分区和 dm-verity 的哈希对不上,又卡 Google logo。

正确姿势:全量刷——super_empty 重置逻辑分区表 + 一次性刷入全部逻辑分区(system / system_ext / product / vendor / vendor_dlkm / system_dlkm)+ 匹配的 vbmeta。

verity 哈希对不上时,用:

1
avbtool make_vbmeta_image --flags 3 ...   # flags 3 = 同时禁用 verity 和 verification

生成一个禁用校验的 vbmeta 刷进去。(这台设备上 fastboot --disable-verity 会报 “Failed to find AVB_MAGIC at offset 0”,对自编译的 vbmeta 不奏效,只能用 avbtool 这条路。)

闯过这四关,设备终于在 enforcing 下正常开机进系统了。试拨——拨通了,振铃,接起来……


五、最后一公里:接通了,但对方听不到我

通话能建立了,但出现了诡异的现象:

  • 我能听到对方(下行 OK);
  • 对方听不到我(上行无声)
  • 几十秒后通话因为 RTP Timeout 自动挂断。

抓媒体层日志,铁证如下:

1
2
3
numRtpPacketsTransmitted = 0          ← 上行一个 RTP 包都没发出去
txSilenceDetected = true ← 上行被判定为静音
audio_route: Apply path: null-source -> voice-call-uplink ← 麦克风没接到上行通道!

null-source -> voice-call-uplink——上行通道的音频源居然是个空源,麦克风根本没被路由进去。

根因:装了 HIDL 的音频配置,却跑着 AIDL 的 HAL

adevtool 提取出来的音频 HAL 是 AIDL 版本(android.hardware.audio.service-aidl.aoc)。但是 device 配置里,引入 AIDL 音频配置文件的那段被一个 release flag 门控着:

1
2
3
ifeq ($(RELEASE_PIXEL_AIDL_AUDIO_HAL),true)
# 引入 gs-common/audio/aidl.mk → 装 AIDL 音频配置
endif

RELEASE_PIXEL_AIDL_AUDIO_HAL 这个变量在公开的 AOSP 树里根本没定义。于是这段被跳过,系统装的是旧的 HIDL 音频配置mixer_paths.xml),却跑着 AIDL 的 HAL。两者不匹配,AIDL HAL 找不到正确的混音路径(mixer path)把麦克风接到通话上行通道,于是上行成了空源。

修法:强制走 AIDL 音频路径

device/google/caimito/device-komodo.mk 里直接绕过那个未定义的 flag:

1
USE_AUDIO_HAL_AIDL := true

这样就会引入 gs-common/audio/aidl.mk,装上正确的 AIDL 音频配置

  • mixer_paths_aidl.xml(AIDL 的混音路径表)
  • AoC(Always-on Compute,Tensor 的音频协处理器)的上行路由配置 uplink_*_config.pb
  • 与官方一致的 audio_platform_configuration.xml

顺手把之前手动加的、现在由 aidl.mk 原样提供的音频 sepolicy 删掉去重(否则会撞 “Multiple same specifications” 编译错误)。


六、收工:完整双向通话跑通

重新编译、刷入、enforcing 模式试拨。这次的媒体层日志:

1
2
3
4
5
6
7
audio_route: Apply path: microphones -> voice-call-uplink-0   ← 麦克风接上了!
numRtpPacketsTransmitted = 954 → 1090 → 1364 → 1707 ← 上行 RTP 在持续发出(之前是 0)
txSilenceDetected = false ← 上行不再是静音
numRtpPacketsReceived = 2472, numVoiceFrames = 2459 ← 下行也正常
codecType = 2 ← AMR-WB 宽带语音
callDuration = 56234 ← 通话稳定 56 秒+
averageRoundTripTime ≈ 80ms

无 1610,无 RTP Timeout,上下行都有声,通话稳定不掉。

至此目标达成:一个从源码自编译的、除 telephony 外保持纯净的 AOSP 15,在 SELinux enforcing 模式下,跑通了完整的双向中国移动 VoLTE 通话。


七、复盘:整条链路的坑

把整个过程串起来,能打通电话需要同时满足的条件,按从下到上的顺序:

问题 修法
配置 Google 默认关闭中国移动 VoLTE + 配置 checkin 被墙 用 CarrierSettings + 正确的运营商配置 .pb
电话栈 手搓移植不连贯 → 1610 adevtool 成套连贯提取(含原生库)
构建 vendor.img 没生成 恢复 BoardConfigVendor.mk / device-vendor.mk
SELinux HAL 二进制 file_contexts 漏标 hal_audio_default_exec 等标签
SELinux service_contexts 缺 IAudioExtension hal_audio_ext_service
刷机 super/verity 不一致 全量刷 + avbtool --flags 3
音频 装 HIDL 配置跑 AIDL HAL → 上行空源 USE_AUDIO_HAL_AIDL := true

几点最大的体会:

  1. 先在原厂验证可行性,再啃自编译。 否则你根本不知道是软件问题还是硬件/SIM 问题,方向都找不准。
  2. 不要手搓移植成套的、互相咬合的闭源栈。 电话栈这种东西,用 adevtool 这类工具成套连贯提取,比手工挑 APK 靠谱一万倍——手工挑必漏原生库。
  3. enforcing 下的 boot 失败,先怀疑 sepolicy。 file_contexts 标签错、service_contexts 缺服务,都会让 HAL 起不来从而卡 boot。permissive 能起、enforcing 起不来,基本就是 sepolicy 缺口。
  4. “接通了但没声音” 要看媒体层日志的 audio_route。 null-source -> voice-call-uplink 这种就是音频路由没接上,往往是 HAL 类型(HIDL/AIDL)和配置文件不匹配。

下一步是把这套电话栈连同其他定制(双击熄屏、root、电池百分比、GApps)一起,折叠进一个自签名的 user 版,baked 进去做成日用机。那是另一篇的故事了。


整个过程跨了好几个晚上,一层坑套一层坑。但最后看到 numRtpPacketsTransmitted 从 0 开始往上涨、对方说”听得到了”的那一刻,还是挺爽的。所谓”做不到”,很多时候只是没找对工具和没把每一层都啃透而已。