FxxkMoondrop

Moondrop 蓝牙耳机助手(LSPosed / Xposed 模块):耳机连接时自动弹出 Fast Pair 卡片,并通过 GAIA BLE 协议直连耳机,读取左右耳电量、控制降噪。

作者:bqj6666 | 版本:3.2.10(versionCode 330) | 许可证:GPL-3.0(见 LICENSE)

本仓库是 LSPosed 模块索引 / 发布页,不含源码。 完整源码、构建方式、Issue 与最新版本请前往源码仓库:


功能

  • 蓝牙监听 + GAIA 直连:BLE GATT 直连耳机,读取左右耳电量、控制降噪
  • ANC 型号档案库:AncProfileLib 按设备名自动套用实测设备码映射,未实测型号回退默认映射
  • FastPairHook(LSPosed,注入 Google Play 服务):借道 GMS 的 BLE 扫描动态发现耳机 LE 地址并推送
  • 弹窗模式:Google Fast Pair 半屏弹窗(注入 GMS 的 HalfSheetActivity);弹窗「设置」按钮跳转系统蓝牙设备详情页
  • 系统设置蓝牙详情注入(LSPosed):Hook 系统蓝牙设备详情页,注入降噪与功能控制面板(抗风 / 增益 / 指示灯);未连接时整块收起,耳机未就绪时用官方加载行原位占位
  • 官方空间音频 / 头部跟踪两行(LSPosed):详情页直接使用系统自带的「空间音频」与「头部跟踪」开关,不再自绘控件 —— 官方那两行在本机被系统判定为不可用并自行移除,模块放开该判定后交由官方渲染,勾选态与点击接到耳机端;仅对支持的耳机接管,他牌耳机原样交还官方
  • Google 官方耳机控制面板桥接(LSPosed):Hook GMS 的 Hearable Controls 链路(GFPS 消息组 0x08)—— 以 DexKit 特征定位官方 ANC 子模块、放开 Fast Pair 缓存门禁,在意图入口与管理器发送出口两处接住面板点击(降噪 / 通透 / 音量条 / 提示音和振动),翻译成 GAIA 请求真实下发;并把应用侧真实档位注入官方 DataStore,官方界面高亮与实际一致
  • 三协议自动识别:GAIA V3(BLE)/ GAIA V4(RFCOMM/SPP,布丁)/ Moondrop 私有 9ECA0000 自动路由
  • 9ECA 私有协议客户端:音源切换 / EQ / MIC / SN(复用同一 GATT 连接)
  • 设备通知:耳机电量与降噪控制合并为一条常驻通知(只显示左右耳,不含充电盒);档位按钮带大图标、当前档位高亮、颜色随系统动态取色,按钮按设备实际支持的能力生成
  • 无 Root 模式:未检测到 Root 时自动回落为仅通过通知栏与 App 主界面控制降噪(GAIA BLE 直连本就不需要 Root),所有 Root 依赖项静默停用,不报错、不弹窗
  • M3 界面:主页 / 设置 / 关于,Material 3 + 动态取色
  • 设置页功能开关:官方集成(官方降噪面板 / 蓝牙详情页面板)、通知(电量通知 / 通知内降噪控制)可单独停用
  • 权限检测(整页二级界面):6 项实时检查,按必要权限(蓝牙 / 通知 / GAIA 直连)与可选权限(电池白名单 / Root / FastPairHook 模块)分组呈现,一键跳转修复;结论只看必要项
  • 日志抓取(设备适配):一键收集系统信息 / 应用设置 / 蓝牙 / 运行环境 / logcat 六类日志打包为 ZIP
  • 保活默认常开且无需 Root:开机自启 + 30 秒看门狗 + 系统电池优化白名单(只主动询问一次);已 Root 时静默追加增强。另有后台隐藏可选开关
  • 使用引导:首次启动自动展示,横滑分页、每页一个功能分区(关于 / 权限申请 / 连接管理 / 系统集成 / 耳机控制 / 适配与诊断 / 欢迎使用);页内开关与设置页同源,改完立即生效;看完或跳过后不再自动弹出,设置页最底部可随时重看
  • 显示层中英文切换:语言偏好(跟随系统 / 中文 / English)

软件截图

主页概览 设置 关于 Fast Pair 弹窗 设备通知
主页 设置 关于 Fast Pair 设备通知

支持设备(水月雨全系列)

本项目面向水月雨(Moondrop)全系列蓝牙耳机。兼容性判定基于蓝牙传输层与服务指纹,不依赖型号名。因此只要主控为高通 QCC 或中科蓝讯(Bluetrum),水月雨全系列蓝牙耳机均在支持之列;未收录型号也会在首次连接时自动探测,通过后永久放行。

  • 已实机验证:梦回2 / Golden Ages 2(GAIA)、梦回1979、太空漫游2 / Space Travel 2(9ECA)、太空漫游2 ULTRA、U.C.T.S(MD-OWS-014,GAIA;硬件无降噪)、布丁 PUDDING(GAIA V4)、猫咖 MOCA
  • 理论上支持:爱丽丝 / 火花 / 旅行者 / 猫饼 / 音乐胶囊 / 超声波 / 知更鸟 / 太空漫游(一代)/ 方糖 / 羽翼 EDGE 等水月雨全系列型号

更多细分的型号、主控与协议依据,见源码仓库 FxxkMoondrop README。

安装

  1. 下载并安装 APK(见源码仓库 Releases)
  2. 在 LSPosed 中启用,作用域:com.google.android.gms(可选 com.android.settings)
  3. 首次启动会展示使用引导,可在其中直接完成权限授权(设置页「检查权限」亦可一键跳转修复);已跳过的话可在设置页最底部重新打开
  4. Root 可选:没有 Root 也能用 —— 未检测到 Root 时自动进入无 Root 模式,照常读电量、切降噪(走 GAIA BLE 直连),仅官方面板注入、弹窗图标自定义、Root 保活等增强项停用

Fast Pair 弹窗适配

Fast Pair 弹窗依赖完整的 Google Play 服务(GMS),能否弹出与手机系统的 GMS 完整程度有关,与耳机型号无关,模块本身无需单独安装 GMS 组件。

系统 Fast Pair 弹窗 说明
类原生 / 原生(完整 GMS) 功能完全正常 已实测
ColorOS(OPPO / realme / 一加) 需额外模块 搭配 oplus-cn2global + Luckytool 后可用
其他系统 理论支持 支持完整 GMS 即支持,待实机验证

说明

  • 本项目应用代码部分使用 AI 辅助生成,已人工审查与实机验证,但仍可能存在逻辑错误或兼容性问题,请谨慎使用,风险自负
  • 项目借助逆向 Moondrop App 私有接口实现,仅供学习研究使用,不含 Moondrop App 任何代码 / 资源 / 反编译产物,请勿用于商业用途

Releases

10/7/2026, 1:20:31 PM

正式发布 340-3.2.20

  • 提交:b44eda32a785d6288ec3d149a1b7d813ebe96a9e
  • 触发:tag 340-3.2.20
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 331-3.2.11 以来的全部变更。

3.2.20 (340)

本版集中在连接链路的收敛与界面规范对齐。

两处「会影响其他品牌耳机」的缺陷已修:Google 原生配对卡片被误认领、
以及对未收录型号的协议探测无休止重试(会打断其他耳机的头部追踪)。

另修 issue #12 的水月雨 知更鸟 Robin:先是因一次空的服务发现结果
被判为「不支持协议」并永久拉黑(表现为除电量外全部不可用),
修复后又发现它的档位取值与名义默认映射错位 —— 点「透传」发出的档位
设备不认,按钮形同摆设。本版把它正式收录进型号档案。

修复:不再影响其他品牌耳机

  • Google 原生配对卡片被误认领。Google 给真正支持 Fast Pair 的耳机
    (Sony / Pixel Buds / Nothing 等)弹的原生卡片,与模块自绘弹窗用的是
    同一个 HalfSheetActivity。此前的归属判定有一级只看到「距模块上次拉起
    弹窗不足 8 秒」就认领,缺少正向证据 —— 用户在这 8 秒窗口内触发原生配对,
    那张卡片就会被当成我们的:图标被覆盖、电量被替换,其「连接」按钮还会被
    短路成模拟连接,直接干扰对方配对。现在要求 Intent 里必须带模块专属标记
    才认领,其卡片一律走官方原生流程。
  • 对未收录型号的协议探测不再无休止重试。名字未命中的已连接音频设备
    会被放行做一次协议指纹探测,但探测失败并不拉黑,而检测服务每 5 秒轮询
    一次 —— 于是对同一台设备无限重试。很多 TWS 的头部追踪跑在同一条 BLE
    链路上且只接受一个中央连接,反复的 GATT / RFCOMM 连接尝试会把那条链路
    拽断,表现就是「装上模块后其他耳机头部追踪时断时续」。
    现在每个设备每会话最多探测 2 次,用满即停。
  • 已判为不支持的设备不再被反复重连。重连逻辑此前不检查设备判定结果,
    即使设备已被证伪拉黑,仍每 30 秒重连一次。现已按判定结果收敛。
  • 以上限制不影响水月雨设备:名字命中 moondrop / golden ages /
    水月雨 / robin / 知更鸟 的设备完全不受次数约束。

修复:空的服务发现结果不再被判为「不支持」

  • issue #12 现场:GATT 连接后仅数十毫秒就返回空的服务列表,
    模块据此判定「协议指纹证伪」并永久拉黑该设备 —— 此后连探测机会都没有,
    表现为除电量(走系统通道)外全部不可用。
  • 空列表是传输层异常(GATT 未就绪、系统 GATT 缓存为空、连到了错误的
    LE 地址),不是协议指纹,不能据此判断设备是否支持协议。这一点与代码内
    既有注释自相矛盾,属实现与设计不一致。
  • 现在空列表先延迟重发服务发现自救(最多 2 次),仍失败才退回 RFCOMM;
    只有「确实拿到了服务、但两个协议服务都不在」才算证伪。

修复:连接的一处 GATT 泄漏

  • 扫描命中后的连接入口在覆盖连接对象前没有关闭旧对象,
    可能让旧连接永不释放 —— 它仍挂在协议栈上继续收通知,
    且与新连接共用同一个回调,导致每条通知被处理两遍、
    耳机端维持两条多余的 BLE 链路。另两个连接入口早已修好,这是最后一处漏网。

修复:设备被跳过时主动提示

  • 此前设备被判为「不支持」是静默的:界面看不出异常,用户只看到连不上,
    自然不会想到去点「刷新状态」自救。现在概览页会列出被跳过的设备名,
    并指向一键重新检测。
  • 该提示右上角可关闭,关闭后同一设备不再重复打扰;
    出现新的被跳过设备时才会再次提示。

新增:知更鸟 / Robin 型号档案

  • 收录 ROBIN'S EARPHONES(issue #12 的设备)。此前它未入库,走名义默认映射
    1=关/2=降噪/3=透传/4=抗风,而这副耳机实际只有「降噪」与「透传」两档、
    取值是 1=降噪/2=透传。
  • 错位的后果:耳机在透传时界面显示成「关闭」;点界面的「透传」会发出
    dev=3 —— 该耳机只认 1/2,命令被忽略,按钮自然毫无反应。
    用户的原话是「透传按钮貌似是个摆设」。
  • 入档后:降噪=1、透传=2 一一对应;并把不存在的「关闭」「抗风噪」两档标记为
    不支持,界面不再显示这两个点了没反应的按钮。
  • 依据:issue #12 第二次反馈的日志。整段使用期间耳机只在 dev=1 与 dev=2
    之间切换,且用户明确说明「耳机本身也就只有降噪和透传模式」。
  • 顺带修好一处共性缺陷:音频管理(AudioCuration)路径此前完全不读型号档案,
    一律宣告 4 档 —— 于是任何只有 2~3 档的 GA2 系耳机都会多出死按钮。
    现在该路径与 ANC_V2 路径一样按档案收敛档位,主界面 / 通知栏 / 快捷磁贴
    / 官方面板共用同一份判据。
  • 未收录型号不受影响:无档案时仍宣告基础 4 档。

变更:界面按 Material 3 规范对齐

  • 卡片圆角统一为 12dp。此前同一屏里有 20 / 24 / 28 三种,都不在 Material 3
    的卡片规范档上(Card 用 medium = 12dp)。对话框仍为 28dp(另属一档)。
  • 卡片改为每组一张、组内行间细分隔线。此前是每行独立卡片 + 12dp 间距,
    整页碎成十几块、边距参差。
  • 卡片补回描边(1dp,低对比色),对齐 Material 3 的 Outlined 卡片规范。
  • 页面底色与卡片拉开层级。浅色下原底色与纯白卡片亮度仅差约 2%,
    卡片边界几乎不可见;现采用 Material 3 官方中性色阶。
  • 分组标题改用主色,左边缘与卡片内图标对齐在同一条基准线。
  • 行内边距与字号对齐 LSPosed 设置页(取自其实际使用的库与实测坐标)。

变更:其他

  • 界面文案去除 emoji,全应用统一为文字表述。
  • 小米 / HyperOS 系统集成的设置项隐藏。该功能尚未在真机验证过,
    且在非小米设备上只显示一行置灰说明、白占一屏。相关代码完整保留。
  • HyperOS 风格界面(Miuix)停用。功能与交互均已跑通,但观感与参考
    实现仍有差距,暂不投入使用。代码完整保留,Material 界面不受影响。
  • 颜色取值不再硬编码,统一收敛到主题色令牌中。

3.2.12 (332)

本版新增均衡器(预设可切换、参量均衡只读)、快捷设置磁贴与弹窗时机控制,
并把设置页按功能重新归类、全应用文案统一为系统设置风格。
未触碰蓝牙连接链路 —— 与 3.2.11 逐字节相同,3.2.9 的防死机修复完整保留。

新增:均衡器

  • 概览页新增「均衡器」入口,仅在耳机提供 Moondrop 私有 9ECA 服务时出现。
  • 预设可切换:读出耳机内置预设数量,点选即下发,失败自动回滚高亮。
  • 参量均衡只读:读取 32 段参数,绘制频响曲线(对数频率轴),并列出增益非零的频点。
  • 增益单位按协议中 preGain 同族的 0.01 dB 解释。该单位尚未在真机逐档核对,
    页面底部已注明 —— 本版只读不写,读错也不影响耳机现有设置。
  • 预设只显示「预设 N」不显示名称:设备只回下标不回报名称,名称表是各厂商自定义的。
    官方 App 的图标名暗示了一组顺序,但无法从协议证实,猜错会让用户以为选了「流行」却听到别的。

新增:快捷设置磁贴

  • 下拉快捷面板新增「降噪」磁贴,一次点击循环切换降噪档位。
  • 档位不硬编码,与通知栏、主界面、官方面板共用同一份能力判据;
    耳机未连接时磁贴置灰,点它则打开应用。
  • 档位变化时磁贴随之刷新,与通知栏挂在同一个刷新点上。

新增:弹窗时机

设置 → 功能 → 连接弹窗,总开关与四项时机收在一处:

  • 弹出时机:等耳机就绪(读到左右耳电量)或连接后立即。
  • 延迟弹出:不延迟 / 1 秒 / 2 秒 / 5 秒。
  • 锁屏时不弹出、横屏时不弹出:两项默认关,即默认行为与加这些开关之前一致。

时机判据放在弹窗收口处而非触发处 —— 延迟期间熄屏或转屏时,出口处的判断才是真正生效的那次。

修复:无模块环境(无 Root 模式)下,这两项开关显示为禁用却仍能拨动 ——
行的 isEnabled = false 传不到子控件,开关是作为独立控件塞进行里的,必须禁用开关自身。

变更:设置页归类与文案

  • 「连接弹窗」总开关从「官方集成」移入独立的「连接弹窗」块,与四项时机同处一卡。
  • 「显示抗风噪按钮」从「功能」末尾移入「自定义映射」—— 它管的是档位呈现,与那组同源。
  • 全应用文案统一为系统设置风格:副标题只说明「这项是什么」,不再包含操作指导与条件句;
    条目名称去掉第二人称与口语表达。中英文同步。

变更:预发布附带更新日志

预发布发布页此前只有一句提交链接,每次都要自己点进 commits 翻。
现在与正式版走同一个脚本:以最新正式版为基准,附上尚未发布的 CHANGELOG 段落。

变更:更新日志与检查更新分工

两者此前是同一件事,内容完全重复。现在各司其职:

  • 更新日志只显示本机这一版改了什么(按本机版本号定位到对应的正式版发布),
    此前显示的是「最新版」的内容 —— 与检查更新的结果重复,而用户点它想看的恰恰是自己装的这版。
  • 检查更新查出新版本时,直接展示新版本的更新说明与下载按钮,不用再跳去看日志。
  • 更新日志取不到时如实说明(预发布构建在仓库里没有对应的正式版标签属于这种情况),
    不再悄悄回退去显示别的版本的内容。

取更新日志的链路同时补了两道保险:atom 兜底(该接口不占 API 配额,
而未认证的 REST 配额实测会被日常点击打满、返回 403)与按版本缓存
(同一版本的说明不会变,看过一次就不再打网络)。

变更:README 徽章改为动态

版本徽章此前是写死的(停留在 3.2.10,早已过期)。现改用 shields.io 的 GitHub 集成,
实时反映最新正式版。选它而非 dynamic/json 是因为前者由 shields 侧带凭据与缓存取数,
后者要 shields 直接请求 GitHub API,共享出口 IP 容易吃到限流、变成 error 徽章。

Assets

1

9/28/2026, 6:36:51 PM

Warning

仍在使用 3.2.8 及更早版本的用户请立即升级。
那些版本存在恶性缺陷:用本模块连接耳机后,偶发导致耳机死机 / 自动关机 / 断开连接。
缺陷取决于触发时序,所以表现为偶发;一旦触发,耳机即无响应或直接掉电关机。
该问题自 3.2.9 起已修复,但缺陷版本的用户不会收到任何提示,所以这条警示继续保留。

本版新增联网检查更新(默认关闭),并把应用内的更新日志改为只从 GitHub 读取。
同时按 Material 3 规范修掉一批界面缺陷:弹窗按钮错位、弹窗标题居中、下拉菜单过窄等。


正式发布 331-3.2.11

  • 提交:8d0fe5d0158bb0df94ffd395463d2541c2468e96
  • 触发:tag 331-3.2.11
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 330-3.2.10 以来的全部变更。

3.2.11 (331)

新增:联网检查更新(默认关闭)

  • 「设置 → 更新」:联网检查更新开关(默认关)+ 更新通道(正式版 / 预发布)。
  • 「关于 → 项目 → 检查更新」:手动检查;发现新版本可直接点「下载」交给系统浏览器。
  • 「关于 → 项目 → 更新日志」:读取 GitHub 的发布说明,展示本版变更。
  • 引导页新增「更新」一页,与设置页开关双向同步。
  • 默认关闭。关闭时不建立任何网络连接 —— 不是「连了但不发数据」,是根本不连。
  • 只访问 GitHub 公开接口,不发送任何设备信息、不收集数据、不带 token。
  • 结果缓存 6 小时:未认证接口限流为每小时 60 次,缓存可避免反复请求撞上 403。

变更:更新日志改为纯联网

  • 移除应用内置的更新日志副本(Changelog.kt)。它与仓库的 CHANGELOG.md 是两份数据,
    每次发版都要手工同步,长期必然漂移;现在只剩一个权威来源 —— GitHub 上的发布说明。
  • 「检查更新」「更新日志」只在联网开关打开时显示。关闭时整个入口隐藏,
    不再摆出「点下去必然失败」的按钮。

修复:Material 3 规范与动效

  • 弹窗按钮错位:「检查更新」弹窗的「下载」按钮此前被追加到「关闭」的下一行,
    两个动作被拆成上下两块。现收进同一行、右对齐、间距 8dp,确认动作在最右。
  • 弹窗标题居中:标题居中而正文左对齐,两条起始线对不齐。改为与正文同一左边界。
  • 下拉菜单过窄:选项文字很短时(如「正式版」「预发布」)菜单缩成一条窄缝。
    现按 M3 规范补足最小宽度。
  • 下拉选中不刷新:「更新通道」选完看不出任何变化,必须切换页面才更新。
    根因在通用下拉行内部 —— 它只改了自己的局部状态,靠调用方各自刷新,
    而调用方并非都刷新了。已收口到一处,所有下拉一并受益。
  • 行组显隐动画:开关控制的行(更新通道)此前是硬切出现与消失,
    现统一为淡入 + 上滑 8dp,与设置页既有做法共用同一组 Motion token。

构建与发布

  • 预发布改为手动触发:此前任何一次 push 到 main(包括只改一行 README)都会自动创建
    一个 ci-N 预发布并同步镜像,Releases 列表很快被无意义的构建淹没。
    现在 push main 仍会构建(编译坏了要立刻知道),但发预发布需要在 Actions 页手动 Run workflow。
  • 正式版发布流程不变:push 版本号 tag 即发布。

Assets

1

9/27/2026, 9:21:07 AM

Warning

请所有用户升级到 3.2.10(或至少 3.2.9)。
3.2.8 及更早版本存在恶性缺陷:用本模块连接耳机后,偶发导致耳机死机 / 自动关机 / 断开连接。
根因是模块会与耳机同时挂起多条 BLE 链路,并在一秒内反复拆建连接,把耳机固件逼到崩溃。
缺陷取决于触发时序,所以表现为偶发;一旦触发,耳机即无响应或直接掉电关机。旧版本请不要再使用。

承接 3.2.9 的严重缺陷修复。本版把更新日志内置进应用、把「3.2.8 及更早版本存在恶性缺陷」的警示置顶到更新日志与发布页(让旧版用户能看到),
并修掉四处界面缺陷:弹窗进出场动效实际未生效、切换主题或 AMOLED 后页面跳回顶部、
引导页内容刷新后跳回顶部、主界面降噪按钮在 GAIA 尚未连接时就显示为可用。


正式发布 330-3.2.10

  • 提交:69f32e08a95947d8ae4441722773f01526b9eff3
  • 触发:tag 330-3.2.10
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 329-3.2.9 以来的全部变更。

3.2.10 (330)

新增:应用内更新日志

  • 「关于」页 → 项目 → 更新日志,直接查看本版变更与已知缺陷警示,无需跳浏览器。
  • 数据源集中在 Changelog.kt(中英双语),升级后一眼能看到「本版改了什么」与「是否需要立刻更新」;
    完整版本历史仍在仓库的 CHANGELOG.md。
  • 旧版本缺陷警示在弹窗顶部用 error 色 + 淡底单独成条,不与普通条目混排。
  • 内容短时弹窗贴合高度、超长才内部滚动,不写死高度。

发布侧:恶性缺陷警示置顶

  • tools/release_notes.py 支持把版本段落开头的 GitHub 警示块(> [!WARNING] 等)
    自动提到发布页最前面 —— 放在「更新日志」正文里,用户要滚过标题才看得到;
    恶性缺陷的提示必须第一屏可见。不是警示块开头的版本段落完全不受影响(已用 3.2.8 回归验证)。
  • 同步回填了已发布的 329-3.2.9 发布页(主仓与镜像仓),旧版用户在我们的发布页同样能看到该警示。

修复:弹窗动效、切主题跳页、降噪按钮可用态

  • 弹窗的进出场动画此前根本没生效:缓动被写成 <set> 里的内联 <pathInterpolator> 元素,
    而 AnimationUtils 只认 set / alpha / scale / rotate / translate,解析直接抛
    Unknown animation name,WMS 最终拿到 null 动画 —— 表现就是弹窗「啪」地出现、
    关闭时硬切消失,比系统默认弹窗还廉价。缓动改为独立的 @interpolator 资源引用,
    缩放幅度按 M3 规范取 0.8 ⇄ 1.0(进场 300ms / 退场 200ms),并做了抓帧回归:
    开合过程都能看到连续渐变帧,不再是单帧突变。
  • 切换主题不再跳回页面顶部:各页主 ScrollView 是代码动态构建的、没有 id,
    Activity 因切换主题 / AMOLED 重建时,系统的 view state 机制无从匹配,滚动位置必然丢。
    固定 id 后可被自动保存与恢复;大标题栏也在重建后补一次同步,不再停在展开态压住正文。
  • 引导页不再跳回顶部:权限页与欢迎页的内容每次刷新都整块重建(removeAllViews),
    内容高度瞬间归零会把 scrollY 一起夹到 0。现在重建前记住位置、重建后接回。
  • 降噪按钮的可用性判据修正:原先拿 serviceState()(「后台监听服务是否在跑」,
    enable 默认 true ⇒ 几乎恒为 ✅)冒充 GAIA 连接状态,于是蓝牙刚连上、GAIA 尚未握手的
    窗口期里,降噪按钮已经显示成可用,点下去却毫无反应。改为以 GaiaBleClient.isConnected()
    为准 —— 等 GAIA 真正连上再放行按钮。

其他

  • 3.2.9 的连接修复与验证结论全部继续有效。本版对 GAIA 的改动只涉及界面可用性判据,不碰连接与协议逻辑。

Assets

1

329-3.2.9

Stable

9/27/2026, 9:08:00 AM

Warning

所有用户请立即更新到 3.2.9 或更高版本。
3.2.8 及更早版本存在恶性缺陷:用本模块连接耳机后,偶发导致耳机死机 / 自动关机 / 断开连接。
根因是模块会与耳机同时挂起多条 BLE 链路,并在一秒内反复拆建连接,把耳机固件逼到崩溃。
缺陷取决于触发时序,所以表现为偶发;一旦触发,耳机即无响应或直接掉电关机。旧版本请不要再使用。

修复严重缺陷:本模块偶发导致耳机死机 / 自动关机 / 断开连接。根因是连接管理存在竞态,会让应用与耳机之间同时挂起多条 BLE 链路、并在一秒内反复拆建。另新增「连接弹窗」总开关,并清理若干文档与死代码。


正式发布 329-3.2.9

  • 提交:16bc1551c31a9ea7868c58bb80889c41778b9638
  • 触发:tag 329-3.2.9
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 328-3.2.8 以来的全部变更。

3.2.9 (329)

严重修复:偶发把耳机连到死机 / 关机 / 断开

现象:用本模块连接耳机后,有时耳机会直接死机、自动关机或异常断开。

取证(真机实测,非推测):

指标 修复前 修复后
并存的 GATT 连接数 2(其中一条存活 11 分钟未释放) 1
1 秒内的链路重建次数 5 1
每条耳机通知被处理次数 2 1
蓝牙栈内累积的 ATT 通道 8 条同时挂在耳机上 0(随连接正常释放)
  • dumpsys bluetooth_manager 的 GATT Client Map 显示两个 app 记录并存:
    app_if: 28 transport AUTO 与 app_if: 29 transport LE,从连接建立起一直存活到进程被杀。
  • 蓝牙栈日志出现 8 条 ATT 通道被一次性本地终止:
    GATT_CONN_TERMINATE_LOCAL_HOST ... att_id: 44,46,47,48,49,50,52,54。

根因(三处,均为连接管理的重入竞态):

  1. GaiaBleClient.retryConnect() 覆盖 gatt 却不关闭旧连接
    该函数直接 gatt = device.connectGatt(...),不像 connect() 那样先 disconnectInternal()。
    被覆盖的旧 BluetoothGatt 永不 close(),仍挂在协议栈上继续接收耳机通知,且与当前连接
    共用同一个 gattCallback —— 这就是「并存 2 条链路」和「每条通知被处理两遍」的来源。
  2. 延迟重连任务无去重、无取消
    retryConnect() 每次调用都新投一个 15 秒后的 disconnect + connect 任务且从不撤销。
    被连续调用 N 次,就会在 15 秒后并发执行 N 次链路拆建。
  3. 轮询侧无「在途」判据,周期性重建链路
    HeadsetDetectService 每 5 秒轮询一次;链路正在建立时也会再走一遍 disconnect + connect。
    且同一副耳机可能以两个地址(BR/EDR 与 LE,或 A2DP / HFP 两条 profile)各占一个条目,
    一轮里先后发起两次连接 —— 第二次必然先拆掉第一次刚建好的链路(实测 37 ms 内两次 connectGatt)。

影响链:耳机固件被要求同时维持多条 GATT 链路、并承受每秒数次链路建立 / 拆除,
部分固件处理不当即崩溃 → 表现为死机、关机、断开。因依赖扫描与超时的触发时序,故为偶发。

修复:

  • retryConnect() 增加在途守卫:已有活跃 gatt 时直接返回,不再覆盖式重建。
  • 延迟重连任务改为单实例 Runnable,投递前先撤销上一次;disconnectInternal() 一并撤销待执行任务。
  • 新增 GaiaBleClient.isConnecting(),轮询侧在「链路正在建立」时跳过,不再重复发起。
    仅在确实存在活跃 gatt 时跳过 —— gatt 已关闭的真空闲态仍照常重连,不影响断开后重新连回。
  • 一次轮询最多发起一次 GAIA 连接(此前同轮可能两次)。

验证:受控复现(force-stop 后重启连接,采集 75 秒)—— 并存 GATT 连接 1 条、
connectGatt 仅 2 次、每条 RX 只处理 1 次、耳机全程保持 STATE_CONNECTED 未死机;
单测 66 例 0 失败;改动未引入任何新编译告警(强制全量重编,告警集合与改动前逐条一致)。

新增:连接弹窗总开关

  • 设置页 → 功能 → 官方集成 → 「连接弹窗」,默认开启。
  • 关掉后不再弹 Google Fast Pair 卡片(图标 / 电量 / 降噪按钮),通知栏与主界面控制不受影响。
  • 实现为单点收口:PopupGate.showConnectedPopup() 是弹窗广播的唯一发送处,
    四条路径(立即 / 延迟 / 超时 / GAIA 就绪)都汇聚于此,一处判断即覆盖全部。
  • 与同组的「官方降噪面板 / 蓝牙详情页面板」一致,在模块未激活时置灰。

其他

  • 清理死代码与死声明:删除构建脚本已不再引用的 xposed-api-stub.jar;
    移除 AndroidManifest 中被 AGP 8 忽略并每次告警的 package 属性(改用 namespace,取值相同);
    移除源码中并不存在、全仓零引用的 .PopupActivity 声明。
  • uses-sdk 的 targetSdkVersion 由 34 对齐为 36(与 DSL 一致,此前每次构建都会告警并被忽略)。
  • 文档修正:构建链版本(AGP 8.6.1 / Kotlin 2.3.21 / compileSdk 35)、组件表、
    测试用例数(20 → 66)、日志类别数、依赖清单、支持设备口径(改为水月雨全系列)、
    作用域说明(澄清 com.android.bluetooth 与官方 App 包不在作用域内)。
  • 架构文档移除逐模块行数表:该类精确数字每次改代码即失效且无读者价值,改为只列职责。

Assets

1

328-3.2.8

Stable

9/26/2026, 11:34:51 AM

正式发布 328-3.2.8

  • 提交:785c984c67d701c0ea09ca385a3323d33dfc920a
  • 触发:tag 328-3.2.8
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 327-3.2.7 以来的全部变更。

3.2.8 (328)

界面全面对齐 Material 3 规范(M3 Expressive 主题 / Material Symbols 图标 / 官方组件替换自绘控件),并顺手修掉 3 个真实的取色与对比度缺陷;构建链升至 AGP 8.6.1 + Kotlin 2.3.21。

界面:对齐 Material 3 规范

  • 主题切到 M3 Expressive(material 1.14.0 提供 Theme.Material3Expressive.DayNight.NoActionBar):
    按钮形状与按压形变、开关、对话框、底部导航、动效曲线整体切到 expressive 规范;
    颜色仍由 themes.xml 的 M3 语义色 token 覆盖,不受影响。
    副作用(预期内):底部导航高度 81dp → 65dp,符合 expressive 的 64dp 规范。
  • 图标统一到 Material Symbols:32 个图标从 Material Icons(24 网格)换成 Material Symbols
    (Rounded / weight 400 / fill 0,960 网格),与已有的 ic_anc_* / ic_track_* 同代。
    此前项目混用两代图标集,笔画粗细与圆角风格不一致。
    保留 ic_gain_1/2/3 为自绘:官方 signal_cellular_alt 系列底部对齐,排成一行会高低不齐。
  • 自绘控件换官方组件:三处自绘 Dialog → MaterialAlertDialogBuilder(净删 40 余行手搓视图);
    主页刷新条与权限页圆形加载由 ProgressBar → 官方 LinearProgressIndicator /
    CircularProgressIndicator;真正耗时的后台任务(root 探测、模块 PING、权限检查)接入官方
    LoadingIndicator;弹窗图标选择对话框改官方外壳 + 统一列表行。
  • 尺寸与排版:按钮触控 40dp → 48dp(M3 最小可点尺寸)、圆角同比 20dp → 24dp;
    21 处不在规范内的字号归位到 M3 typescale。
  • 动效 token 化:新增 M3Ui.Motion(duration token SHORT2/3/4、MEDIUM1/2 + easing token
    emphasized-decelerate / emphasized-accelerate),7 处裸数字动画归位;
    约定「进场 enter()、退场 exit()」——进场从容停下、退场果断离开,混用会显得迟钝。

修复

  • 动态取色在浅色主题下静默失效(2 处):surfaceContainerHighest 的浅色分支用了
    system_neutral1_90,ControlPanel 用了 system_neutral1_80。Android tonal palette
    只有 0/10/50/100/200…/1000,这两个 tone 并不存在,getIdentifier 返回 0 → 永远走 fallback,
    等于这两格从未跟随壁纸。改用最接近的有效 tone。
    另:ControlPanel 的「禁用灰」在两个主题下方向相反,单个固定 tone 表达不了,已按主题分别取值。
  • 通知按钮文字不可读:DeviceNotif 的 onContainer fallback 误写成 container 的值
    (深色下二者相等,文字与底色同色);onPrimary 也未按主题分支。
    已与 ThemeUtil.Palette 的 M3 角色取值对齐,并给出正确的 fallback。
  • 选中态在深色主题下对比度不足:选中态容器色是 primary,前景与描边此前一律硬编码白色。
    浅色主题 primary 是深紫,白前景正常,所以这个缺陷一直没暴露;深色主题 primary 是浅色
    (实测 #ffbd836e),白图标压在同为浅色的底上对比度严重不足。M3 规定「容器上的内容用对应的
    on* 角色」,已改用 onPrimary;并连带修掉图标缓存键(原用「是否等于白色」判选中态,
    前景一改,深色下缓存永不命中、每次重建 drawable)。ControlPanel 同样两处一并处理。
    OnboardingActivity 成功勾选的深色底色是浅绿 #8FD89B,配白勾对比度约 1.8:1,改为按主题取对比色。
  • 明确不改:FastPairHookEntry 里的白色常量。那些是①生成的默认占位图标
    ②克隆宿主按钮样式,属「覆盖在 Google UI 上、必须与宿主观感一致」的场景,不属本模块主题体系。

构建链

  • AGP 8.5.2 → 8.6.1、compileSdk 34 → 35、material 1.13.0 → 1.14.0、
    androidx.core 显式 1.16.0(material 1.14.0 → core 1.16.0 → minCompileSdk 35 且 minAGP 8.6.0)。
  • Kotlin 1.9.22 → 2.3.21,含两处必要迁移:
    kotlinOptions.jvmTarget 在 Kotlin 2.3+ 由警告升级为错误,改到顶层
    kotlin { compilerOptions { jvmTarget.set(Jvm_17) } }(必须放在 android {} 之外);
    PrefsProvider 10 处 arrayOf(...) 显式化为 arrayOf<Any>(...)
    (Kotlin 2.3 下 reified 参数被推断为 Comparable<*> & Serializable 导致编译失败)。
  • 新增 tools/check_vectors.py:矢量图标几何自检(校验 viewBox 与网格归位是否一致)。

验证

assembleRelease BUILD SUCCESSFUL;单测 66 例 0 失败;gradle 警告 0 条;
APK 装机 user0 / user10 双用户成功、启动无崩溃;
GMS hook 实测 onModuleLoaded / onPackageReady 正常,9 个 hook 点 + 4 个接收器全部就位
(确认 Kotlin 升级未影响生成的字节码行为)。

Assets

1

327-3.2.7

Stable

9/21/2026, 3:38:21 PM

正式发布 327-3.2.7

  • 提交:b405db13f2542f16765454931cd1191fe167470c
  • 触发:tag 327-3.2.7
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 326-3.2.6 以来的全部变更。

3.2.7 (327)

主界面降噪区块按设备能力显隐:硬件没有降噪的型号不再挂着点了没反应的按钮。

修复:主界面无条件显示降噪控件(issue #9)

  • 问题:像 U.C.T.S(MD-OWS-014) 这类硬件本身就没有降噪的耳机,主界面仍然
    无条件显示「降噪控制」标题与整排降噪按钮 —— 用户点了没有任何反应,与"这台耳机不支持降噪"
    的事实自相矛盾。
  • 对照:弹窗与通知栏本来就按能力处理 —— 弹窗在能力状态为「无 ANC」时隐藏整条
    模式栏;通知栏按 supportedUiModes() 生成按钮,无 ANC 时自然为空。只有主界面漏了,
    三处表现不一致。
  • 修复:主界面降噪区块(标题 + 按钮行)在「已连接且能力探测明确判定无 ANC」
    (CapabilityProbe.status() == 2)时整体隐藏。
    • 未连接、或能力尚未探明(status == 0)时维持既有行为:显示但置灰 ——
      避免连接过程中先隐藏再出现的闪烁,也避免把"还没探到"误当成"不支持"。
    • 有能力(status == 1)的设备表现完全不变。
  • 取证(issue #9 日志):
    CapabilityProbe: probe: len=27 truncated=false ancPath=-1 features=0,1,3,5,6,13,14,15,16,20,21,22,27
    —— 能力集合里没有 ANC(2) / AudioCuration(8) / ANC V2(32),ancPath=-1,判定确实无降噪。

文档:U.C.T.S(MD-OWS-014)加入「已实测」设备表

  • 依据 issue #9 的实机报告:命中 GAIA、识别与控制正常、左右耳电量正常、弹窗正常、
    手势不受影响;该型号硬件本身不含降噪,因此降噪类功能不可用属预期而非缺陷。
  • 该报告同时确认了 3.2.7 的显隐修复有必要:能力集合里没有 ANC 相关特征,
    主界面此前却仍显示降噪控件。

附带确认:失效 LE 地址淘汰机制在实战中生效

  • 同一份日志里出现了 3.2.6 新增的自愈日志:
    protocol: cached LE addr unusable, dropped: 26:03:27:B7:F0:15 ——
    该用户的耳机 LE 地址缓存多次连不上后被自动淘汰并重新发现,随后
    probe: 正常拿到能力集合,连接恢复。这为 3.2.6 的改动提供了真实设备上的验证。

Assets

1

326-3.2.6

Stable

9/20/2026, 3:19:18 PM

正式发布 326-3.2.6

  • 提交:7dd76e61d13006266c6c79d0790e63a09331841f
  • 触发:tag 326-3.2.6
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 325-3.2.5 以来的全部变更。

3.2.6 (326)

修复:首选候选是失效 LE 地址时原地打转,永远换不到有效地址(耳机一直连不上)。

修复:LE 地址失效后不推进候选,导致连接死循环

  • 问题:onConnectionStateChange 的重试/候选推进逻辑此前只在异常断开
    (status != GATT_SUCCESS)时执行。而连到一个已失效的 LE 地址时,
    底层是「连不上 → 断开回调 status=0」—— 属于正常断开码,于是那一整块逻辑被跳过:
    既不算失败、也不推进候选,下一轮轮询又拿同一个地址去连。
    日志特征:反复出现 connectGatt(auto) <同一地址> (cand 0/N),候选索引永远停在 0。
    耳机换过 LE 地址(蓝牙重启、重新配对)后就是死循环,用户感知为「耳机一直连不上」。
  • 修复(两处):
    1. 断开处理条件放宽为 status != GATT_SUCCESS || !everConnected ——
      本次会话还没成功连上过时,status=0 也走候选推进;
      「连过之后正常断开」的既有行为不变(避免正常断连后疯狂重连)。
    2. 候选重排不再每轮重置:原来每次轮询都会把缓存 LE 地址提到首位、并把起始索引
      打回 0,于是候选推进的结果被反复覆盖 —— 这才是「停在失效地址上打转」的直接原因。
      改为仅在本轮尚未尝试过(attemptCount == 0)时才做这个优先重排。

Assets

1

325-3.2.5

Stable

9/20/2026, 3:03:32 PM

正式发布 325-3.2.5

  • 提交:a30f9faae08240e9f15705ec695813aa127150c7
  • 触发:tag 325-3.2.5
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 324-3.2.4 以来的全部变更。

3.2.5 (325)

连接提速:耳机连接广播到达时立刻开始连 GAIA,不再干等轮询周期。

优化:耳机连接后立即开始连 GAIA(不再等 5 秒轮询)

  • 问题:系统已经把 ACL_CONNECTED / A2DP / HFP 的连接广播免费送到应用进程,
    但接收端此前只用它触发弹窗,不用它触发 GAIA 连接 —— 连接只靠 5 秒轮询周期发现。
    而 GMS 那条唤醒链走的是 am broadcast(需要 root),一旦不可用就只能白等整个轮询周期,
    用户感知就是「耳机连上了,半天不出电量」。
  • 修复:连接广播里增加一次「立即唤醒检测服务做增量轮询」,复用既有的 BT_EVENT
    通道与 onStartCommand 分支,不新增机制;1 秒节流避免 ACL / A2DP / HFP 连发时重复唤醒。
    无 root 环境下同样生效。
  • 实测:装机后 GAIA 就绪约 2.3 秒(重启 app 冷路径),connectGatt 仅 2 次、连接稳定。

说明:两项曾考虑但未采用的改动

  • GATT 关闭顺序:业界资料指出 Android 15+ 上 disconnect() 与 close() 连在一起会
    滞留连接句柄、拖慢重连,建议先 disconnect 再等 STATE_DISCONNECTED 回调后才 close。
    按此实现延迟 close 后实测出现连接风暴(连上即断、每 0.1 秒重连一次、永不就绪)——
    原因是延迟期间旧客户端仍存活,与随后新建的连接争抢同一台 LE 设备。已完整回滚,
    维持原「disconnect 后立即 close」的写法:在「断开后马上重连同一设备」的场景下,
    及时释放客户端比等回调更重要
    。
  • 扫描命中早停:通用扫描原本等满 8 秒才交付候选。考虑到该路径仅在无 LE 缓存时走到、
    且缩短收集窗口可能漏掉更优候选,收益有限而未采用。

Assets

1

324-3.2.4

Stable

9/20/2026, 5:37:15 AM

正式发布 324-3.2.4

  • 提交:fe41eba318362467e912814154eb569ee60477b9
  • 触发:tag 324-3.2.4
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 323-3.2.3 以来的全部变更。

3.2.4 (324)

Hearable Controls 链路(含音量面板)对其他耳机零输出:观测日志也按目标地址收敛。

改进:音量面板 / 发包链路的日志与探针按目标地址过滤

  • 背景核查:音量面板与提示音和振动面板属 GMS Hearable Controls,本模块启用后接管点击。
    核查其转发与注入路径 —— forwardEntryIntent、forwardOfficialSend、injectState、
    probeOfficialGet 全部以 aliasesSnapshot()(目标地址别名集)为前置门禁,
    非目标设备一律 chain.proceed(),功能上不影响其他耳机。
  • 但发现两处观测点没过滤:
    1. dvzo 意图入口的探针日志无条件打印 —— 其他耳机每次面板操作都会在 GMS 日志里
      留下一行我们打的记录;
    2. dwaa.m 发包出口的日志只按事件码与 1.5 秒节流过滤,会把任意设备的地址(MAC)
      与报文明文打印出来
      —— 用户提交日志包时会连带泄露其他设备的 MAC。
  • 修复:新增 isOursAddr(addr),两处观测点均改为只对目标设备记录。
    至此该链路对其他耳机是「功能零介入 + 日志零输出」。

Assets

1

323-3.2.3

Stable

9/20/2026, 5:28:26 AM

正式发布 323-3.2.3

  • 提交:f0cb4cd8441aceabcfabeed7849dff777101baa2
  • 触发:tag 323-3.2.3
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 322-3.2.2 以来的全部变更。

3.2.3 (323)

文档修正:音量面板 / 提示音和振动面板的接管路径写清楚了。

文档:音量面板的接管路径此前描述不准确(ARCHITECTURE.md)

  • 前一版门禁总表里写成「系统音量面板未被 Hook」,这不准确。
  • 实际情况:音量面板与提示音和振动面板都是 GMS 的 Hearable Controls 面板,由本模块的
    feat_official_panel 开关启用后交给官方渲染,档位点击落在 dvzo 意图入口,
    由本模块接住并翻译成 GAIA 下发给耳机。它不是独立的 Hook 点,而是与官方 ANC 面板同一条链路。
  • 因此它与其他耳机的关系同样遵循既有门禁:别名集不含目标地址时,
    forwardEntryIntent / forwardOfficialSend 直接返回,官方原有行为不变。
  • 另:确认音量面板本身(系统 VolumePanel)没有独立 Hook。

Assets

1

322-3.2.2

Stable

9/20/2026, 5:18:09 AM

正式发布 322-3.2.2

  • 提交:9afd051a22f32768c71045e6a70ad8cd2b75159b
  • 触发:tag 322-3.2.2
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 321-3.2.1 以来的全部变更。

3.2.2 (322)

弹窗「设置」键按当前卡片对应的耳机跳转;文档补齐「非目标设备零介入」门禁总表。

修复:弹窗「设置」键可能跳到另一副耳机

  • 设置键此前用「已配对列表里第一个水月雨」解析地址 —— 一台手机上配过多副水月雨时,
    弹窗出现在第二副上、点设置却会跳到第一副的详情页。
  • 改为先按弹窗自己的设备名去已配对列表匹配地址,匹配不到才退回原宽松逻辑。

文档:新增「非目标设备零介入」门禁总表(ARCHITECTURE.md)

  • 逐条列出全部 Hook / 注入点与其归属判据,便于核对「其他 Fast Pair 耳机是否会被影响」。
  • 结论:所有注入与监听都以设备名或目标地址为前置条件,判定不通过即原样放行官方逻辑;
    系统音量面板未被 Hook。

Assets

1

321-3.2.1

Stable

9/20/2026, 5:01:50 AM

正式发布 321-3.2.1

  • 提交:c9a1856249a2311d8f957a7ce66bf447a11ac89a
  • 触发:tag 321-3.2.1
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 320-3.2.0 以来的全部变更。

3.2.1 (321)

弹窗不再误伤其他 Fast Pair 耳机;修多设备切换时「新的连不上 / 一副断另一副也断」。

修复:Google 原生 Fast Pair 卡片被当成自家弹窗动手

  • 问题:给真正支持 Fast Pair 的耳机(Pixel Buds / Sony / Nothing 等),Google 会自己弹一张
    原生卡片,用的就是本模块自绘弹窗依赖的 HalfSheetActivity。此前 Activity.onResume
    的 hook 只判「类名里有没有 HalfSheet」,是就把实例记成「我们的弹窗」,接着在 400ms 后
    执行图标、电量、降噪按钮、设置按钮四处注入;dtes.f 改标题与 dthi.O、dthi.q
    换图片这两条渲染链路同样只认类名。更严重的是 View.performClick 里对 central_btn
    的接管:只要 id 命中就 return true 短路掉原生点击并改跑自绘的假连接流程 ——
    等于把人家的配对按钮吞了。
  • 影响面:装了模块的设备上,接任何真正支持 Fast Pair 的耳机都会命中;即使从没连过
    水月雨耳机,图标、电量、按钮三处注入也不依赖名字、照样执行。
  • 修复:引入弹窗归属判定。判据分两套,分别对付两条路径:
    • onResume / performClick 走 Intent 标记(EXTRA_OUR_SHEET):自绘弹窗是模块自己
      构造 Intent 拉起的,打上标记;Google 原生卡片没有这个标记。这里必须走 Intent 而非静态字段 ——
      弹窗渲染发生在 GMS 的另一个进程里(触发进程 startActivity、渲染进程跑 onResume),
      静态字段跨不了进程(电量 extra 正是因此才经 Intent 传递)。sHalfSheetActivity
      此后只在本模块弹窗上被赋值,其余读取点(注入、高亮、能力刷新、假连接流程)因此天然不碰别人的卡片。
    • 渲染回调走设备名(shouldTouchSheet()):dtes.f(改标题)、dthi.O 与 dthi.q(换图片)
      发生在 onResume 之前、且在渲染进程里拿不到触发进程的静态字段,所以用卡片数据里的设备名
      作判据 —— 名字是本模块支持的耳机才动手,其余一律不碰。
    • performClick:归属判定之外再校验被点的视图确实长在本模块弹窗的窗口里
      (rootView === decorView),否则原样放行原生点击。
    • sOurSheetLaunchAt(8 秒认领窗口)仅作同进程场景的兜底,跨进程时天然为假、不会误放行。
  • 对自家设备零影响:只加门禁、不减功能,注入时机与内容不变。

修复:多设备切换

  • 换耳机连不上:链路空闲但 deviceAddress 还残留上一副的地址时,pollConnected 走的是
    gaia.connect(deviceAddress),也就是旧地址 —— 结果永远在重连上一副,新戴的这副一辈子连不上。
    改为连接本次检测到的目标;与残留地址不同时先断掉残留再连。
  • 一副断、另一副跟着断:断开回调里对 lastSeen 的差集无条件执行 disconnect(),
    任何一副离开都会把 GAIA 链路整个掐掉。改为按地址比对,只有离开的正是当前链路那一副时才断。
  • 处理顺序确定化:pollConnected 的候选集合原本是 HashSet,同时连着多副时
    「谁先被处理、GAIA 最终连上哪一副」不可预期。改用 LinkedHashSet 并按地址排序遍历,行为可复现。

Assets

1

320-3.2.0

Stable

9/18/2026, 11:25:16 AM

正式发布 320-3.2.0

  • 提交:f64bfa9260deab09428a8dc60a47e1f93720ff08
  • 触发:tag 320-3.2.0
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 310-3.1.0 以来的全部变更。

3.2.0 (320)

空间音频与头部跟踪改用系统蓝牙详情页里官方自带的那两行开关(不再自绘),
追踪档位不再卡在「关闭」;降噪按钮顺序在主界面、通知栏与弹窗三处统一;
新增首次启动的使用引导(设置页最底部可重看)。

新增:使用引导(7 页)

  • 首次启动自动展示:横滑分页,每页一个功能分区 —— 关于、权限申请、连接管理、系统集成、
    耳机控制、适配与诊断、欢迎使用。看完(或跳过)后不再自动弹出,设置页最底部随时可重看。
  • 第一页只有「跳过」能提前退出:不设返回箭头,系统返回键只回退到上一页,首页不退出,
    避免误触直接跳过整段引导;末页「开始使用」正常完成并回到主界面。
  • 权限页就地授权:与「权限检测」页同一套判定,点条目直接前往授权;已就绪项以绿色圆形勾标记。
  • 各页开关与设置页同源:写同一份偏好、走同一套副作用(起停监听服务、刷新通知),改完立即生效,
    不存在第二套状态。
  • 末页「欢迎使用」:汇总权限、FastPairHook 模块与引导完成状态。
  • 措辞为中性书面语,不使用表情符号。

调整:权限分为必要与可选两类

  • 「权限检测」页与引导的权限页都按必要 / 可选分组呈现:
    必要 —— 蓝牙权限、通知权限、GAIA 直连(缺失时对应核心功能完全不可用);
    可选 —— 电池优化白名单、Root 权限、FastPairHook 模块(只影响后台留存与系统集成)。
  • 结论只看必要项:权限检测页标题、引导末页「欢迎使用」的就绪状态均以必要项为准,
    可选项缺失不再被当成「权限没配好」;缺失时只在副标题里如实提示数量。
  • 引导权限页与末页的就绪标记改为绿色圆形背景勾,直径由 14dp 放大到 30dp;
    同时修掉「徽标被行容器压成图标本身大小」的问题(尺寸原先落在会被覆盖的那一层)。

调整:空间音频 / 头部跟踪改用官方那两行

  • 现象与背景:系统自带的 BluetoothDetailsSpatialAudioController 在本机恒判不可用
    (getImmersiveAudioLevel()==0),于是把自己刚建好的两行又移除,详情页只剩我们自绘的控件。
  • 做法:放开官方的可用性判定,让官方自己渲染与维护那两行;勾选态与点击接到耳机端(GAIA)——
    空间音频开关对应我们的「空间音频开 / 关」,头部跟踪开关对应三档追踪里的「全方位 / 30°」。
    两行仅在 GAIA 就绪时出现,耳机断开或未就绪时不显示、也不可点。
  • 删除自绘控件:详情页面板不再自建空间音频行与追踪三档,卡片只剩抗风、增益、指示灯。
  • 只对 Moondrop 耳机生效:Spatializer 相关 hook 全部按设备地址判定,
    非本模块耳机(含系统原生就支持空间音频的他牌耳机)一律原样交还官方,不会把人家本可用的开关弄没。

修复:打开空间音频后追踪档位停在「关闭」

  • 现象:打开空间音频,耳机端回报的追踪档位是 0(关闭),界面显示为「关闭追踪」,
    与「空间音频已开」矛盾。
  • 做法:确立不变量 —— 空间音频开着时不会是「关闭追踪」。读到 0 档即自动补为 30°;
    但用户在软件内明确把追踪切到过「关闭」时尊重其选择,不再自动补档。
    只有「本来在 30° / 全方位、用户自己切到关闭」才算手动关,本来就关着再点一次不算表态。

调整:降噪按钮顺序三处统一

  • 主界面、通知栏与 Google 弹窗的档位按钮统一为「降噪 / 关闭 / 通透 / 自适应 / 直播 / 抗风」,
    与官方 GFPS Hearable Controls 通知对齐;顺序表收敛为唯一出处,抗风与自适应的可用性门控不变。

文档

  • README 补充设备详情页截图与赞赏入口。
  • 文档全量同步到 3.2.0:README / README.en 版本号与发布徽章、功能列表(官方空间音频两行、使用引导、必要/可选权限、保活默认常开、日志六类)、版本历史补 3.0.1~3.2.0;ARCHITECTURE 新增「空间音频 / 头部跟踪改用官方两行」章节并补 OnboardingActivity;DEVELOPMENT 的发版同步项由四处改为五处;模块仓 README 同步更新。

Assets

1

310-3.1.0

Stable

9/17/2026, 5:17:01 PM

正式发布 310-3.1.0

  • 提交:e474690058f12babd495b2ff99916508873a8677
  • 触发:tag 310-3.1.0
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 305-3.0.5 以来的全部变更。

3.1.0 (310)

蓝牙设备详情页大改:把官方自己的行放出来(HD 音频 / 通话 / 媒体音频 等),
我们的面板与官方降噪切片各归其位;面板跟着连接状态显隐 —— 未连接时整块收起,
连上但 GAIA 还没就绪时用官方加载行在原位占位。

修复:详情页「相关工具」下方一大片空白 / 面板被挤下去或消失

  • 现象:进入 Moondrop 设备详情页往下翻,官方「相关工具」那一栏出现一大片空白,把下方的
    Moondrop 面板整体往下挤,面板有时还直接消失;翻页过程中偶发重复的一整行内容。
  • 根因(logcat 实测,逐行打了 RecyclerView 子行的高度/适配器位置/所属偏好):
    1. 详情页用行视图承载面板,官方重排(updatePreferenceOrder)会把行视图回收给别的条目,
      我们挂上去的面板跟着残留 —— 同一个面板会在其它位置(如官方「实时字幕」那一行)再画一份,
      那一行高度被撑成面板高度(812px),残留行之间还出现 1445px 的空档。
    2. 适配器/布局管理器重排没落地时,列表会保留这些残留行,表现为「一大片空白」。
  • 做法:
    1. 给所有官方条目补一条清理:绑定到非本模块 key 的条目时,先把残留的面板视图从它的
      itemView 上摘掉 —— 面板只可能出现在它自己那一行。
    2. 官方重排 + 显形跑完后,只让列表 重新布局一次(requestLayout,立即 + 400ms + 1200ms),
      让残留行被回收、行距恢复。
      注意:不能用 notifyDataSetChanged —— 实测它会把官方「实时字幕」那类自绘条目重新膨胀成
      一张没有文字的空白卡片(控制器不会重跑),那正是空白的一大来源。
  • 实测结果:残留行 0、面板行 found=1、官方「实时字幕」正常渲染,重复行与 1445px 空档消失。

调整:详情页面板(抗风开关 / 边距 / 位置)与官方降噪切片的状态

  • 抗风改成开关:原来那个小药丸按钮换成与「空间音频」完全同一行的开关(共用同一个开关行构件,
    拿的也是设置 App 自己的开关控件),样式不会再各自漂移;显隐仍以主界面同一套判据为前提
    (设备实际支持该档 + 用户偏好),未就绪时置灰。降噪档位切换仍交给官方切片,我们只留这一个快捷键。
  • 抗风开关只在「降噪 / 抗风」档出现:抗风是降噪的加强档,所以只有处于降噪档时才提供这个开关 ——
    打开即切到抗风档,关闭即回到降噪档;若直接切到通透或关闭,开关自动弹回关闭,并连同整行一起隐藏
    (行后的间距随整块一起收掉,不会残留一段空白)。
  • 设置项跟着改名说明:「显示抗风噪按钮」的描述改为讲清抗风 = 降噪的加强档 —— 详情页快捷开关只在
    「降噪/抗风」档出现,切到通透或关闭会自动收起;关掉该项后弹窗、主界面与详情页都不再提供抗风。
    「蓝牙详情页面板」的描述补上「未连接时整块收起、未就绪时用官方加载行原位占位」。
  • 边距:面板左侧不再自己加留白(偏好行本身已带官方那套左右边距),卡片内再收 8dp,
    「抗风 / 空间音频 / 追踪模式 / 增益」与官方切片内容同一条竖线,元素不贴边。
  • 位置固定:官方那次重排会把「不在它名单里的项」收进它自己生成的分类,面板位置因此会漂
    (有时紧跟官方操作按钮、有时沉到底部)。现在在官方重排跑完之后,把我们的几行摘成屏幕根的直接子项,
    按官方「操作按钮」那一行的 order 依次给号(PreferenceGroup 本来就按 order 决定插入位置),
    顺序固定为:官方操作按钮 → 官方降噪切片 → 官方加载行 → 我们的面板 → HD 音频等官方配置行。
  • 官方降噪切片的状态:切片来自设备元数据(机型不支持就既不显示切片也不显示占位,不留一条永远转的进度条)。
    本机还没就绪(GAIA 未完成服务发现,命令发不出去)时,用官方自己的加载行(loading_pref)在切片原位占位,
    切片收起;就绪后切片出来、加载行收起。状态由模块进程跨进程推送,连上后无需重开页面。
  • 顺带修掉一个空白:官方行自带的子视图以前被我们删掉再放面板,行视图被适配器回收给别的条目后
    (实测「实时字幕」),对方重新绑定时找不到自己的控件,整行就变成空白卡片。现在只「藏」不「删」,
    别的条目绑到这一行时先摘掉残留面板、再把被藏的孩子放出来。

新增:设备详情页放出官方自己的行(HD 音频 / 通话 / 媒体音频 等)

  • 背景:官方 BluetoothDetailsConfigurableFragment.updatePreferenceOrder 会把「不在 displayOrder 里」的项
    整体挪进 invisible_profile_category(该分类自身 visible=false),于是官方自己的行全被藏掉;
    ROM 还额外通过 BluetoothFeatureProvider.getInvisibleProfilePreferenceKeys 把
    通话音频 / 媒体音频 / HD 音频 列进隐藏名单(官方日志 Invisible profiles: [...])。
  • 做法:只对 Moondrop 设备 (1) 不去填那两个官方隐藏名单;(2) 在官方那次重排跑完、主线程下一次消息里,
    把 invisible_profile_category 及其内部「有可见子项的」分组显示出来。不搬动子项、不指定具体条目,
    每行出不出来仍由官方各自的 controller 决定(「HD 音频」因此仍然只在连接且支持可选编解码器时出现)。
  • 面板重复行:上述改动会让该页适配器重排后偶尔给我们的面板条目留下残留行(同一面板画两遍)。
    绑定后按父容器把多余的兄弟行折叠成 0 高度,只保留当前正在绑定的那一行。
  • 「耳机控制」切片(bt_extra_control)已放出:本 ROM 没有任何代码给它设 URI。
    设备元数据里的 HEARABLE_CONTROL_SLICE_WITH_WIDTH 的 view_width 是空的,GMS 对空宽度直接返回空切片;
    用本机屏幕宽度补齐后 GMS 正常返回内容(实测 items=3),再用官方 SlicePreferenceController
    按官方流程(setSliceUri -> displayPreference -> onStart)挂上即可。地址仍取自设备元数据,不写死。
  • 面板重复行:上面的改动会让该页适配器重排后给我们的面板条目留下残留行(同一面板画两遍,
    表现为元素挤在一起)。改为:绑定后就地扫描根视图里所有带面板标记的视图,只保留当前这一行、
    其余整行 GONE(只压高度挡不住内容溢出)。实测 found=2 hidden=1 → 单份。
  • 加载占位:官方 loading_pref 是配置数据未到位时的滚动进度条,随隐藏分类一并放出来后很扎眼,
    显式保持隐藏。

修复:设备详情页凭空多出「自适应」档

  • 问题:只有 0..3 四档的设备(如 Golden Ages 2,走 AudioCuration 路径)在能力探测
    尚未成功时(未连接 / RFCOMM 失败),控制面板会多出一个「自适应」按钮 —— 点了没反应。
  • 根因:可用档位未知(modes 为空)时,可见性规则 !knownModes || modes.contains(i)
    对所有列都为真,自适应(4) 也被兜底显示;而 buildAncCard 里那行「初始 GONE」
    紧接着就被首次 refreshAncCard 点亮,实现与注释相左。
  • 修复:可见性规则收敛到纯函数 AncProfileLib.ancColumnVisible(uiMode, knownUiModes, showWind) ——
    能力未知时只兜底通用基础四档(新增 BASIC_UI_MODES),自适应(4) 必须有能力证据才出现。
    同时 supportedUiModes() 的 ANC_V2 未收录型号兜底由 0..5 收窄为 BASIC_UI_MODES,
    不再向官方音量面板 / 通知 / 弹窗宣告「自适应 / 直播」这两个拿不到证据的档位。
  • 影响面:布丁等有档案的 ANC_V2 设备行为不变(仍宣告 [关,降,透,抗,自适应]);
    主界面降噪卡本就只建 4 档,通知与弹窗早有能力判据,本次只补齐设备详情页这一处漏网。

修复:未连接时不再留下占位与面板(3.1.0 收口)

  • 现象:耳机断开之后,官方那条加载行还一直在原处占位;面板也赖着不走。
  • 根因:显隐只看「GAIA 是否就绪」,没看「耳机是否连着」——断开后 GAIA 必然不就绪,
    于是永远落进「占位」那一档。
  • 做法:三行的显隐改成看三个事实:设备元数据里有没有控制切片地址、耳机是否已连接、
    GAIA 是否就绪。未连接 -> 三行全隐藏;已连接但未就绪 -> 官方加载行在原位占位、我们的面板
    出来(控件置灰);就绪 -> 切片出来、加载行收起。规则本身抽成纯逻辑 DetailRows,7 个单测覆盖
    三档与「没有切片地址的机型」「上报顺序抖动」等边界。

修复:蓝牙被关掉后不再报「已连接」

  • 现象:适配器关掉后上层收不到断连回调(实测 GATT 被静默丢弃,只剩写命令失败),
    模块一直认为连着 —— 面板、官方切片、通知都停在旧状态。
  • 做法:连接判据加上「适配器必须是开着的」这一条兜底(拿不到适配器状态时不改变原有判断),
    关掉即视为未连接;详情页另外自己听系统的蓝牙广播(适配器开关 / 链路断开),
    断开当场就把三行收起来,不用退出页面再进来。

修复:详情页现在读得到「隐藏抗风档」这个偏好

  • 以前详情页读的是 Settings 进程里那份并不存在的偏好文件,于是用户关掉抗风档后详情页照样显示。
    现在这个偏好跟着状态一起由模块进程带过来(和其它跨进程配置同一条通道),显示与主界面一致。

Assets

1

305-3.0.5

Stable

9/17/2026, 6:07:23 AM

正式发布 305-3.0.5

  • 提交:f6d945d0f1462f7e20e3a022a1632f9b72d2bc9a
  • 触发:tag 305-3.0.5
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 304-3.0.4 以来的全部变更。

3.0.5 (305)

修复:没有 root 但模块已启用时,依赖模块的功能被整片置灰;模块激活不再与「有 root」混为一谈;
root 探测失败不再成为永久结论,并新增重试入口。

修复:没 root 就被整片禁用(依赖模块的功能不该看 root)

  • 问题:旧判定 EnvProbe.isNoRootMode() = !isRooted() && hookActiveCached() != true,
    而 hook 缓存未探测时是 null —— 一进设置页(此时还没探测过)就会被判成「无 Root 模式」:
    弹窗图标、官方降噪面板、蓝牙详情页面板全部置灰、开关点了没反应,
    后台也不再尝试拉起官方 App。可这三项本来就由 GMS 进程里的 Hook 承担,
    与 App 自身有没有 root 无关:没 root 但 LSPosed 模块已启用 = 照常可用。
  • 修复:
    • 判定拆开:新增 EnvProbe.hookUsable() = 只有确定未激活(false)才算不可用,
      未探测(null)不再当作不可用;设置页只按它置灰,文案改为
      「需要 FastPairHook 模块(在 LSPosed 中启用后可用)」。
    • 设置页首次进入时若 hook 尚未探测,后台补探一次(离开主线程),确认未激活才重刷页面。
    • 真正依赖 root 的两条链路(MoondropBooter 用 su 拉起官方 App、
      LogCollector 用 root 复制到公共目录)改为只看 isRooted(),不再被模块状态牵连。
    • 权限检查「运行模式」改用同一次检查里的实测结果,不再把「还没探测」显示成「模块未激活」。
    • 删除已无调用方的 isNoRootMode() / hasRootEntry() / noRootSummary(),避免再次被误用。

修复:「模块激活」与「有 root」不再混为一谈

  • 问题:EnvProbe.isNoRootMode() 只看 root 探测结果;GaiaBleClient.init 更是
    「没 root 就直接跳过 hook 探测」。可 GMS 桥接与官方面板注入靠的是 LSPosed 模块
    (Hook 跑在 GMS 进程里),App 自身并不需要 root —— 有模块无 root 的设备因此被误判成
    无 Root 模式:跳过模块探测、设置页相关项被置灰、权限检查显示「仅通知栏控制降噪」。
  • 修复:
    • EnvProbe.isNoRootMode() = 无 root 且 hook 未确认激活;hook 缓存未就绪时按旧行为保守处理,
      启动时序不变。
    • GaiaBleClient.init 一律实测 hook(不再因「没 root」跳过),日志如实写 root=… module=…。
    • 权限检查「运行模式」改三态:Root 模式 / 模块模式(有模块无 root:桥接与面板注入可用,
      Root 增强功能不可用)/ 无 Root 模式。

修复:root 探测失败不再永久缓存(未授权时 root 整体隐藏)

  • 背景:未在 Root 管理器里授权时,root 会整体隐藏(FolkPatch 的 pathhide 等),
    App 侧看到的现象和「设备根本没 root」完全相同 —— 因此不能把一次失败当作结论。
  • 修复 RootShell:正结果进程内永久锁定;负结果只保留 10 秒 TTL
    (避免一个 UI 流程里反复 exec),TTL 过后任何调用自动重试;retryNow() 供用户显式动作立即重试。
    hook 探测同样处理(hookActiveCached() / retryHookProbe(),对应「刚在 LSPosed 里启用模块」)。

保活改为「无需 Root、默认常开」(移除设置里的 Root 强力保活开关)

  • 问题:强力保活整套都挂在 Root 上(dumpsys deviceidle whitelist + appops + 写 service.d
    开机脚本),没 Root 就完全用不了;设置页还得让用户自己开、自己确认风险。
  • 修复:新增 KeepAlive,保活改为常开、且不再需要 Root:
    • 开机自启用 BOOT_COMPLETED、看门狗用 30s AlarmManager + START_STICKY(本来就不需要 Root);
    • 电池优化白名单改用系统的 ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 弹窗申请
      (新增权限声明),只主动问一次,避免骚扰;被拒后可在「设置 → 检查权限」里手动再点;
    • 有 Root 时额外静默做 deviceidle + appops 增强(失败不影响,也不再有开关/风险弹窗)。
  • 设置页移除「Root 强力保活」开关与风险确认弹窗;权限检查的「电池优化白名单」变为点击即申请。
  • 至此,应用不再需要直接授予 Root:唯一仍然碰 Root 的可选项(把图标兼容写到 GMS 老路径)已改为增强路径。

修复:弹窗自定义图标「选了没反应」(结果回调丢失 + 必须 Root)

  • 问题一(结果回调):选图用的是 requireActivity().startActivityForResult(...) 配合
    Fragment.onActivityResult。实测(Android 15 / ColorOS)回调根本到不了 Fragment ——
    选完图片什么都没发生。证据:应用 cacheDir 里连中间产物 moondrop_custom_icon.png 都没有生成,
    即 saveIconFromUri 从未被调用。
  • 问题二(必须 Root):图标要写进 GMS 私有目录
    /data/user/0/com.google.android.gms/files/moondrop_icon.png,没有 Root(或 Root 被隐藏)必然失败。
  • 修复:选图改用 registerForActivityResult(GetContent()) 启动器;
    图标改为写本应用 filesDir,由 exported 的 PrefsProvider(openFile)跨进程提供给 GMS 侧 Hook 读取
    (与 show_wind / lang 同一条通道),因此无需 Root;有 Root 时仍额外写一份 GMS 老路径。
    顺带新增 icon_ver,Hook 侧可据此判断图标是否换过。

新增:权限检查里的「重新检测 Root」入口

  • 权限检查新增 Root 权限 一项,显示当前状态与选中的入口(su / kp),点击即在
    后台清缓存并重新探测(探测会 exec、可能弹授权框,绝不在主线程做),完成后回到 UI 线程重渲染。
  • 提示文案据实分两种:见到了二进制但没拿到 uid=0 → 「已找到 root 入口但未取得授权…」;
    完全没见到 → 「未检测到 root 入口。若设备已 root:请先在 Root 管理器中把本应用加入授权列表…」。

Assets

1

304-3.0.4

Stable

9/17/2026, 5:34:21 AM

正式发布 304-3.0.4

  • 提交:ae9ba810b69fc4c1dd9fd17511fcc0f8146da707
  • 触发:tag 304-3.0.4
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 303-3.0.3 以来的全部变更。

3.0.4 (304)

修复:FolkPatch(APatch 系)等使用 kp 作为 root 入口的设备上「识别不到 Root」,以及由此连带失效的 Root 功能。

修复:Root 检测不认 kp(APatch / FolkPatch 系)

  • 问题:EnvProbe.detectRoot() 只探测文件名 su / magisk / magisk64,而
    FolkPatch(APatch 的非并行扩展分支)默认把 root 入口装在 /system/bin/kp ——
    反编译其源码可证:uapi/scdefs.h 中 #define SU_PATH "/system/bin/kp"、
    LEGACY_SU_PATH "/system/bin/su",APatchApp.DEFAULT_SU_PATH = "/system/bin/kp",
    su 仅作旧版兼容路径。这类设备因此被判成「未检测到 Root」,整体掉进无 Root 模式
    (跳过 GMS 桥接探测、停用强力保活与静默拉起等)。
  • 修复:检测标记补 kp;惯例目录补 /debug_ramdisk、/data/adb/{ap,ap/bin,fp,fp/bin,ksu,ksu/bin}。
    注:/data/adb 在真机上实测为 drwx------ root root(普通 App 不可读),
    这些条目只在放宽权限的 ROM 上提供额外命中机会,真正生效的是 /system/bin/kp 这类世界可读路径。

修复:Root 命令执行不再硬编码 su

  • 问题:四处调用均为 Runtime.getRuntime().exec(arrayOf("su", "-c", ...))
    (日志 Root 导出、静默拉起官方 App、强力保活写入/清除、Root 可用性自检)。
    在 su 不存在而只有 kp 的设备上,即便检测通过也会全部静默失败。
  • 修复:新增 RootShell,按 su → /system/bin/su → /system/xbin/su → kp →
    /system/bin/kp → /debug_ramdisk/su → /data/adb/ksu/bin/su 顺序探测,
    取第一个执行 id 返回 uid=0 的入口并缓存。su 排在最前,因此
    Magisk / KernelSU / 旧 APatch 设备选中的仍是 su,行为与旧版一致。
    命令改为合并读 stdout+stderr、带超时(授权框弹出时留有点击时间,不无限等待)。

诊断

  • 日志抓取的「运行环境」段改为 Root: 检测到 | 入口=<su|kp|…>,便于一眼看出选中的是哪个入口。

Assets

1

303-3.0.3

Stable

9/17/2026, 5:09:35 AM

正式发布 303-3.0.3

  • 提交:718cd473031a70a28be850a74cb7c0162ec2292a
  • 触发:tag 303-3.0.3
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 302-3.0.2 以来的全部变更。

3.0.3 (303)

修复:布丁(PUDDING / MD-TWS-056)ANC 命令发不出去(issue #4 复测反馈);补齐 ANC_V2 档位映射与自适应档。

修复:能力响应按「特征对列表」解析,ANC 路径不再被误判为未知(issue #4)

  • 问题:布丁每次连接都回同一帧 23 字节能力响应
    00 1D 01 01 | 00 | 00 02 | 01 01 | 05 01 | 0D 01 | 0E 01 | 0F 01 | 10 01 | 13 01 | 14 01 | 16 01 | 20 01。
    旧实现只认「长度是 4 的倍数」的 32 位位图,见 23 % 4 != 0 即判「位图截断」并把 ancPath 钉回
    UNKNOWN;于是 setAncMode 在发送前就被 dev < 0 拦下(日志 setAncMode: mode 1 not allowed on path -1
    连打 57 次)—— 现象是「降噪点了没反应」,而能力探测其实每次都收到了完整能力表。
  • 依据:反编译官方 Moondrop App 内 Qualcomm GAIA SDK
    com.qualcomm.qti.gaiaclient.core.gaia.qtil.data.GetSupportedFeaturesData:
    byte0 = hasMoreData(0/1),其后每 2 字节一组 featureId | version —— 是特征对列表,不是位图。
    按此解析,布丁的 feature = 0x00 BASIC / 0x0D 电量 / 0x0F 增益 / 0x13 指示灯 / 0x20 ANC_V2
    (无 AudioCuration、无空间音频),与 PuddingPods 文档和本仓库布丁档案完全吻合。
  • 修复:GaiaCommands.parseSupportedFeatures 现在同时支持两种封装 ——
    长度奇数且首字节 ≤1 判为对列表,否则按老位图解析(GA2 等老固件行为不变);
    isFeaturePayloadTruncated 只对位图生效,对列表不再误报截断。

修复:布丁 ANC_V2 档位映射(设备库)

  • 问题:ANC_V2 路径此前是恒等映射,而布丁固件语义是
    00 关闭 / 01 自适应降噪 / 02 通透 / 03 抗风噪 / 04 基础降噪,于是
    「降噪」按钮发成 01(自适应降噪)、「自适应」按钮发成 04(基础降噪)——两档互换。
  • 修复:AncProfileLib 新增 ANC_V2 型号档案表(PUDDING:
    SET UI[关,降,透,抗,自适应,直播] -> [0,4,2,3,1,-1],GET dev -> UI = [0,4,2,3,1])。
    GaiaCommands.ancDevFromUi / ancUiFromDev 在 ANC_V2 路径上支持型号映射,未收录型号仍回退恒等。

界面:自适应档位在所有界面统一可用

  • supportedUiModes() 对 ANC_V2 先问型号档案:布丁宣告 [关,降,透,抗,自适应](不含直播),
    于是官方面板 / 通知 / 弹窗都只出现该设备真能执行的档位;
    官方面板(含音量条入口那套 Hearable Controls)的 ui_modes 宣告随之收窄,
    自适应按钮映射到 dev 01、降噪映射到 dev 04,高亮状态与设备读回一致。
  • 弹窗 mode bar 新增「自适应」按钮:仅当设备能力含该档才出现;能力后到时自动重建 mode bar,
    不再出现「探测完成前开过的弹窗少一档」。
  • 诊断:能力探测结论文本写入 AppLog(probe: len=... truncated=... ancPath=... features=...),
    用户导出的日志里可直接看到探测结果。

加固:Hook 返回值类型安全化(防宿主进程崩溃)

  • 新增 HookGuard:libxposed / LSPosed 的 Hook 桥按目标方法返回类型解包回调返回值,
    目标返回基本类型(如 boolean)而回调给 null 时,桥内解包即抛异常 —— 崩的是宿主进程
    (GMS / Settings / SystemUI),不是模块自己。本模块大量使用「不满足条件就 early-return null
    放行原逻辑」的写法,对 void 方法恰好正确,对少数返回基本类型的方法就是一颗雷。
  • 现有 24 处 early-return 全部改走 HookGuard.nullSafe(chain):由目标方法的真实返回类型决定
    安全值(void / 引用类型 → null,语义不变;基本类型 → 该类型默认值并记警告日志)。
  • HookGuardTest 新增 8 例覆盖这张回落表(void / 引用 / boolean / int / 类型不匹配 / 构造器 / 空 chain)。

界面:版本号去掉 "Alpha" 字样

  • 关于页与主页英雄卡此前固定显示 V3.0.3Alpha。3.0 起已是正式版,改为直接显示 V3.0.3
    (仍以 PackageManager 为唯一来源,不硬编码;旧 alpha 版本号的兼容剥离保留)。

Assets

1

302-3.0.2

Stable

9/17/2026, 12:27:50 AM

正式发布 302-3.0.2

  • 提交:c9c3556273b9d4dcb2001deeef64d85961fe7658
  • 触发:tag 302-3.0.2
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 301-3.0.1 以来的全部变更。

3.0.2 (302)

修复:布丁(PUDDING / MD-TWS-056)等 Classic Bluetooth 设备在 RFCOMM/SPP 上「连得上但控制无效」。

修复:RFCOMM 发送缺少 GAIA 传输帧封装(issue #4)

  • 问题:RFCOMM/SPP 路径一直直接发送裸 PDU(00 1D <cmdValue> <payload>),
    没有官方 GAIA 传输帧头。设备侧解析器读到的首字节是 0x00(非法 SOF),整帧错位,
    于是对每条命令都不回有效应答 —— 现象就是「RFCOMM 已连接、命令发出去了,
    但能力探测一直超时、降噪点击无反应」。日志特征:framer pending 1 bytes (partial frame)
    反复出现、且全程没有一条 RX pdu。
  • 依据:反编译本机安装的官方 Moondrop App 内的 Qualcomm GAIA Client SDK
    (com.qualcomm.qti.gaiaclient.core.gaia.core.transport.TransportProtocol$Rfcomm$Frame.format),
    官方 RFCOMM 封装为 FF | version | flags | length | PDU,其中
    length = PDU 长度 - 4,flags 的 bit0=校验和、bit1=双字节长度
    (双字节长度仅 version >= 4 且长度 > 255 时启用);官方 GaiaFormatter.Rfcomm 默认不带校验和。
  • 修复:新增 wrap() 按上述格式封装;发送封装自适应——先 FRAMED_V4、
    再 FRAMED_V3、最后回退旧的 BARE,每种尝试 3.5 秒,收到第一个有效回包即锁定该封装
    (设备固件差异不硬编码,靠回包自证)。
  • 顺带修复接收路径两个确定性缺陷:
    1. 旧实现 input.read(full, n, avail) 不检查返回值 —— 少读时 ByteArray 尾部
      会留下 0x00 填充,凭空造出假字节污染帧缓冲;
    2. 旧实现固定 sleep(50ms) 后只查一次 available() 判定 burst 结束,分段到达的帧
      会被切碎。改为「累积到线路静默 45ms」再切分,并新增 RX burst(<n>) <HEX> 十六进制诊断日志。

Assets

1

301-3.0.1

Stable

9/16/2026, 4:58:46 PM

正式发布 301-3.0.1

  • 提交:1c44ce1c9dca59a09838429f857983efe97812b5
  • 触发:tag 301-3.0.1
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 300-3.0 以来的全部变更。

3.0.1 (301)

修复:未收录型号被误判为「不支持的设备」而永久拉黑。

修复:协议指纹证伪之前不得拉黑设备

  • 问题:设备识别门禁对「名字不含关键字」的型号(如系统里显示为 Robin's Earphones
    的知更鸟)会放行一次协议指纹探测,由 GATT 服务发现裁定;但拒绝名单的写入点此前是
    无条件的 —— 只要 GAIA / 9ECA / RFCOMM 三条路全失败就写进 rejected,
    而这三条路失败的原因可能是传输层问题(连接超时、status=147、耳机刚出仓未就绪、
    信号差),与设备到底支不支持协议无关。结果是未收录型号撞上一次抖动就被永久拉黑,
    此后不再探测,只能靠主界面「刷新状态」手动清空名单。
  • 修复:引入 protocolRefuted 标记,只有服务发现成功、且确认 GATT 里既无 GAIA 服务
    也无 9ECA 服务
    时才置位(这是「设备确实不支持」的唯一铁证);拒绝名单写入点改为仅在
    该标记置位时执行。传输层失败不再写名单,下一轮 detect 轮询照常重试。
  • 影响面:仅 GaiaBleClient.kt;已实测型号命中名字白名单、本就不走探测路径,不受影响。

Assets

1

300-3.0

Stable

9/16/2026, 3:32:02 PM

正式发布 300-3.0

  • 提交:788d422cf586c6d670b9d932959a4d8b3a0bcd38
  • 触发:tag 300-3.0
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 289-alpha2.54 以来的全部变更。

3.0 (300)

从 alpha2.x 进入正式版:版本号收敛为 3.0,versionCode 300。

新增:设备常驻通知(电量 + 降噪控制合成一条)

  • 原来电量与降噪控制是两条独立通知,现合并为一条:标题为设备名,正文显示左右耳电量
    (按需求不含充电盒),下方一排降噪档位按钮。
  • 按钮不用系统 action:Android 12 起系统只渲染文字胶囊、传进去的图标被直接丢弃,
    而这里要的是像主界面那样的大图标按钮。改用自定义布局 ——
    圆形底 + 大图标 + 文字,当前档位实心高亮,颜色全部走 M3 动态取色
    (与 App 内面板同一套 token:选中 = primary/onPrimary,未选中 = container/onContainer)。
  • 折叠态与展开态分别提供视图:系统给折叠态自定义视图的高度上限约 48dp,
    带上标题与电量行就会把按钮整块裁掉,因此折叠态只放按钮行、展开态给完整信息。
  • 内容不变不重发:每次 notify() 都会让系统重建视图、把用户刚展开的通知打回折叠态,
    这正是「通知自己折叠起来」的来源;现按内容指纹去重。
  • 档位按钮按设备能力动态生成(supportedUiModes()),顺序对齐官方 GFPS 通知:
    降噪 / 关闭 / 通透,其余档位随后。

修复:官方降噪面板(音量条 / 提示音和振动)点击无反应

  • 根因:官方面板的点击走 HearableControlManager 的发送出口,既不经过子模块的
    in()(那条只写 dataStore),也不经过 dwaa.m —— 后者发包前要等
    「上一次 SET 的响应」,而本机没有 GFPS Message Stream
    (EventStreamManager: No available EventStreamMedium),这个响应永远等不到,
    于是发包出口根本不会被调用,现象就是「点了没反应、随后高亮回弹」。
  • 修复:在意图入口(子模块 in())与管理器发送出口两处都接住用户意图,
    把拿到的 GFPS 报文翻译成 GAIA 请求交给 App 真正下发给耳机;两侧都按
    「报文指纹 + 时间窗」过滤掉自己注入的 NOTIFY,不会自触发。
  • 激活重试由「20 × 3 秒后永久放弃」改为「前 20 次 3 秒、之后 30 秒长期重试」——
    Fast Pair 模块是事件驱动加载的,原窗口一过就永久失效,面板会一直显示旧模式且点击无效。

新增:设置页「功能开关」四类开关

  • 官方集成:官方降噪面板、蓝牙详情页面板。
  • 通知:电量通知、通知内降噪控制。
  • 开关经跨进程 SP 读取,hook 侧按开关真实停用(关掉即不注入、不发通知),
    读取失败一律按「开」处理,不因读取异常静默废掉功能。

改进:面板与详情页

  • 降噪档位按设备实际能力动态分配(supportedUiModes()),不再固定四档;
    能力表外的档位不注入官方面板,避免出现点了没反应的档位。
  • 面板内控制项统一以 GAIA 就绪作为可交互判据(比「已连接」更强),未就绪时置灰。

新增:无 Root 模式(未检测到 Root 时自动回落)

  • 项目本体是 LSPosed 模块,但控制耳机的能力从来不需要 Root ——
    读电量、切降噪走的是标准 GAIA BLE 直连(BluetoothGatt)。
    因此在没有 Root 的设备上自动回落为「仅靠通知栏 + App 主界面控制降噪」。
  • 回落时一律静默停用所有 Root 依赖项,不再尝试、不再报错、不再弹窗:
    • su 拉起官方 App(MoondropBooter)——必然失败,直接跳过;
    • 官方面板 / 详情页注入(GMS Hook,需 LSPosed)——设置页对应开关置灰并说明;
    • Root 强力保活——开关置灰并说明,且保留用户原有设置值(临时无 Root 不该改掉它);
    • 弹窗图标自定义——该图标由 GMS 进程内的 Hook 读取(readIconBytes),
      无 Root 时 Hook 不工作,设置页该项一并置灰并说明;
    • 日志抓取里的 su 复制——走非 Root 兜底路径;
    • 启动时的 Fast Pair Hook 探测——跳过那 4 秒必然超时的等待,直接走内置 BLE 自扫。
  • 保证回落链路可用:通知栏按钮被点击时会顺带把服务拉起(幂等),
    因此即便后台进程被系统回收,点一下通知按钮也能恢复 GAIA 连接;
    开机自启与 AlarmManager 保活本来就不依赖 Root。
  • 权限页与设置页会明确显示当前处于「无 Root 模式」及其可用范围,
    避免用户误以为功能坏了。

改进:权限收紧(移除三处从未使用的声明)

  • SYSTEM_ALERT_WINDOW:本模块从不创建悬浮窗 —— 代码里没有任何
    canDrawOverlays / TYPE_APPLICATION_OVERLAY / 添加 overlay 窗口的调用;
    弹窗走的是 GMS 进程内的既有窗口,受 GMS 自身权限约束,与本应用无关。
    该权限此前声明了却一直是 granted=false,权限检测页还据此提示用户去授予,
    属于误导,现一并删除(清单 + 检查项 + 两处跳转分支 + 相关文案)。
  • FOREGROUND_SERVICE / FOREGROUND_SERVICE_CONNECTED_DEVICE 与 service 上的
    android:foregroundServiceType:全仓没有任何 startForeground() / stopForeground()
    调用,运行时 dumpsys activity services 也确认该服务未处于前台状态 ——
    HeadsetDetectService 实为普通后台服务(靠 START_STICKY + AlarmManager 保活),
    这几项声明对运行时零影响。
  • 收紧后清单只剩真正在用的权限:BLUETOOTH_CONNECT、BLUETOOTH_SCAN、BLUETOOTH、
    POST_NOTIFICATIONS、RECEIVE_BOOT_COMPLETED。

改进:设置页分类重组

  • 原「行为」段把官方集成 / 通知 / 保活 / 后台 / 界面元素混在一起,现按功能域拆分:
    外观(主题、取色、AMOLED、语言)、功能(官方集成、通知、抗风噪按钮)、
    自定义映射、后台(监听、隐藏、保活)、诊断(权限、日志、弹窗图标、模拟测试)。

  • 版本号升至 3.0(versionCode 300)

Assets

1

9/16/2026, 12:57:59 AM

正式发布 289-alpha2.54

  • 提交:cb7e49d45fe9d955f88ab9f3d1c844b81784d009
  • 触发:tag 289-alpha2.54
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 288-alpha2.53 以来的全部变更。

alpha2.54 (289)

修复:反复开关「动态取色 / AMOLED 纯黑」导致崩溃

  • 根因(三处叠加,缺一即不崩但隐患仍在):主题开关的 OnCheckedChangeListener 里直接
    Handler().postDelayed({ requireActivity().recreate() }, 350/550) ——
    1. 没有任何取消机制:快速反复开关会排出多个 recreate 回调,逐个执行;
    2. 回调闭包持有的是创建它的那个 Fragment 实例。第一次 recreate 完成后该实例已 detach,
      后续排队的回调再调 requireActivity() 必抛 IllegalStateException: Fragment not attached to an activity;
    3. 没有生命周期清理,视图销毁后回调照跑。
  • 修复:
    • 新增统一入口 scheduleRebuild(delayMs):先 removeCallbacks 再 post,把连续请求合并成一次重建,
      从根上消除「多个 pending 回调」这一前提。
    • rebuildRunnable 执行前用 isAdded / activity / isFinishing 三重检查,任一不满足静默跳过,
      不再依赖 requireActivity()。
    • onDestroyView 取消挂起重建并释放 seedRow 引用。
    • 主题下拉、种子色、语言、重置映射四处直接 recreate 一并收拢到该入口(同样存在 detach 风险)。
    • makeThemeSwitch 的 seedRow 改为局部引用,消除 !! 断言与 parent 检查之间的 TOCTOU。
    • 种子行入场动画同样改为局部引用 + parent 检查 —— onDestroyView 置空 seedRow 后,
      原 seedRow!! 延迟回调会抛 KotlinNullPointerException(本次一并堵住)。
  • 实测:12 次连续点击(296ms 内,全部落在同一个 350ms 窗口)只触发 1 次重建,
    进程存活、无异常(修复前会排队 12 次)。用户真机复测通过。全仓单测 26 例全绿。
  • 版本号升至 alpha2.54(versionCode 289)

Assets

1

9/15/2026, 4:35:55 PM

正式发布 288-alpha2.53

  • 提交:761adbeef27ffa6cddb8902e52c16eb36140287e
  • 触发:tag 288-alpha2.53
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 286-alpha2.51 以来的全部变更。

alpha2.53 (288)

修复:断开连接后英雄卡徽章仍停留在旧编解码器

  • 根因(两处叠加,缺一不可):
    1. codecLabel 为缓存字段,断开时从不清空 —— 徽章继续显示上一次查到的编解码器,与「未连接」的真实状态矛盾。
    2. 断连广播只走 updateAncStatus(不走 updateStatus),而徽章原先只在 updateStatus 内刷新 —— 状态变化根本没触发重算。
  • 修复:
    • linkTypeLabel() 无链路时返回 null(原返回「未连接」字符串)。让调用方直接判断「是否该丢弃缓存」,避免依赖字符串比较。
    • badgeText() 检测到无链路即清空 codecLabel;refreshCodecBadge() 无链路直接返回,不白跑一次 dumpsys。
    • 徽章刷新接入 updateAncStatus,随连接状态广播同步更新。
  • 该缺陷影响 alpha2.52(287),该版本已撤回,请升级到本版。
  • 版本号升至 alpha2.53(versionCode 288)

alpha2.52 (287)

界面按 Material 3 规范统一重构

  • 大标题随滚动收缩:三页统一为 M3 LargeTopAppBar 形态(展开 152dp / 收起 64dp,标题 28sp→22sp)。内容自标题下方穿过、标题钉在上层。ScrollView 必须 clipToPadding=false,否则顶部内边距区成为裁剪区,标题收起后中间会空出一条缝(本次实测踩到并修复)。
  • 切页动效:进入 fadeIn + scaleIn(0.985) 320ms(M3 淡入手感),退出快速淡出;新增 res/anim/m3_page_in.xml、m3_page_out.xml。
  • 英雄卡改强调色:底色 container → primary,文字与图标走 onPrimary,API 徽章反色;新增 heroFg 字段,不再借用功能卡配色。
  • 三选一改下拉:主题、语言由成排按钮改为「行 + 右侧当前值 + 下拉菜单」。菜单出现在手指落点(贴边自动内收),入场 scale 0.8→1 + fade 140ms,选中项 primary 填充 + ✓;尺寸按 M3 菜单规范(文字 14sp / 图标 18dp / 行高 48dp)。
  • 补齐语言卡与「检查权限」卡之间缺失的 12dp 卡间距。

修复:蓝牙设备详情面板退化为单行条目

  • 根因:hook 进 com.android.settings 后,面板构建拿的是宿主 Context。模块与宿主的资源包 ID 都是 0x7f,同一数值在宿主体内被解析成宿主自己的资源 —— 实测 ic_anc_off 撞上 com.android.settings:dimen/animation_max_size,抛 Resources$NotFoundException;异常被 onBindViewHolder 的 catch 吞掉后走 chain.proceed(),渲染成原生单行,表现为「整块面板消失、点击无反应」。
  • 修复:新增 M3Ui.moduleDrawable() —— 模块进程内直接用传入 Context(与改动前逐字等价),hook 进程才 createPackageContext 并缓存。DcIcons 一并改走它(追踪 / 增益 / 指示灯同理会崩);standardSwitch 的 ✓/✗ 拇指图标同类隐患一并堵住。

修复:空间音频开关改用设置 App 原版控件

  • 直接构造 MaterialSwitch 会在 Settings 进程抛 IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat,使整个面板注入失败。
  • 改为 inflate 设置自身的开关布局(按新到旧:settingslib_expressive_preference_switch → preference_widget_switch_compat → preference_widget_switch;API 36 实测命中第一个)。该布局内的 MaterialSwitch 自带 Theme.Material3.DynamicColors.DayNight 覆盖,就是设置页在用的同一个控件;全部失败才退回系统 Switch + M3 配色。

图标

  • ANC 四态与弹窗降噪按钮改 Material Symbols 矢量(关闭 / 降噪 / 透传 / 抗风),取代原先手绘 Canvas 几何;真机确认主界面、hook 面板、GMS 弹窗三处一致。
  • 应用图标全新重构为自适应图标:108dp 画布、中心 72dp 安全区、图形收敛在 66dp 内,含 background / foreground / monochrome 三层(支持 Android 13+ 主题图标)。笔画刻意加粗以适应 24–48dp 的单色渲染;原有位图保留作 API 26 以下回落。

英雄卡徽章改显示当前编解码器

  • 由固定的 API 级别改为显示耳机当前在用的编解码器(LDAC / AAC / SBC / aptX …)。该信息只有蓝牙栈掌握(BluetoothA2dp.getCodecStatus 为 @SystemApi、需 BLUETOOTH_PRIVILEGED),故经已有 Root 通道读取并解析 dumpsys,不维护任何编解码名称表;读不到时退回链路类型(LE Audio / ACL / LE),再退回「未连接」。异步查询 + 5s 节流,不阻塞 UI。

自定义映射交互规范化

  • 降噪 / 增益的裸数字输入框改下拉选择:可选值收敛为固定集合,越界与半截输入不再可能出现;降噪编辑仍四档全量落库。增益第 0 项为「隐藏该档位」(设备码 -1)。
  • 空间音频追踪标签由「每次按键都落库」改为失焦 / 回车才保存,空值视为恢复默认。
  • 「当前型号档案」与「当前:…」两行统一规格(12sp medium / primary)。
  • 修复菜单选中态不跟随:原先把创建时的选中值闭包进菜单,选完重开仍是旧高亮;改为行内维护可变选中态。

其他

  • 主界面「刷新状态」新增线性进度条(BLE 重连需数秒),状态回包即收起,6s 超时兜底。
  • 本轮另含(commit 749f8a6):未收录型号门禁指纹化(issue #5)、主界面电量行未连接仍显示、刷新按钮重试探测。
  • 版本号升至 alpha2.52(versionCode 287)

Assets

1

9/15/2026, 4:19:17 PM

正式发布 287-alpha2.52

  • 提交:de6b89eb3df4d9d3e0fa930ff99b2ff6cd72f534
  • 触发:tag 287-alpha2.52
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 286-alpha2.51 以来的全部变更。

alpha2.52 (287)

界面按 Material 3 规范统一重构

  • 大标题随滚动收缩:三页统一为 M3 LargeTopAppBar 形态(展开 152dp / 收起 64dp,标题 28sp→22sp)。内容自标题下方穿过、标题钉在上层。ScrollView 必须 clipToPadding=false,否则顶部内边距区成为裁剪区,标题收起后中间会空出一条缝(本次实测踩到并修复)。
  • 切页动效:进入 fadeIn + scaleIn(0.985) 320ms(M3 淡入手感),退出快速淡出;新增 res/anim/m3_page_in.xml、m3_page_out.xml。
  • 英雄卡改强调色:底色 container → primary,文字与图标走 onPrimary,API 徽章反色;新增 heroFg 字段,不再借用功能卡配色。
  • 三选一改下拉:主题、语言由成排按钮改为「行 + 右侧当前值 + 下拉菜单」。菜单出现在手指落点(贴边自动内收),入场 scale 0.8→1 + fade 140ms,选中项 primary 填充 + ✓;尺寸按 M3 菜单规范(文字 14sp / 图标 18dp / 行高 48dp)。
  • 补齐语言卡与「检查权限」卡之间缺失的 12dp 卡间距。

修复:蓝牙设备详情面板退化为单行条目

  • 根因:hook 进 com.android.settings 后,面板构建拿的是宿主 Context。模块与宿主的资源包 ID 都是 0x7f,同一数值在宿主体内被解析成宿主自己的资源 —— 实测 ic_anc_off 撞上 com.android.settings:dimen/animation_max_size,抛 Resources$NotFoundException;异常被 onBindViewHolder 的 catch 吞掉后走 chain.proceed(),渲染成原生单行,表现为「整块面板消失、点击无反应」。
  • 修复:新增 M3Ui.moduleDrawable() —— 模块进程内直接用传入 Context(与改动前逐字等价),hook 进程才 createPackageContext 并缓存。DcIcons 一并改走它(追踪 / 增益 / 指示灯同理会崩);standardSwitch 的 ✓/✗ 拇指图标同类隐患一并堵住。

修复:空间音频开关改用设置 App 原版控件

  • 直接构造 MaterialSwitch 会在 Settings 进程抛 IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat,使整个面板注入失败。
  • 改为 inflate 设置自身的开关布局(按新到旧:settingslib_expressive_preference_switch → preference_widget_switch_compat → preference_widget_switch;API 36 实测命中第一个)。该布局内的 MaterialSwitch 自带 Theme.Material3.DynamicColors.DayNight 覆盖,就是设置页在用的同一个控件;全部失败才退回系统 Switch + M3 配色。

图标

  • ANC 四态与弹窗降噪按钮改 Material Symbols 矢量(关闭 / 降噪 / 透传 / 抗风),取代原先手绘 Canvas 几何;真机确认主界面、hook 面板、GMS 弹窗三处一致。
  • 应用图标全新重构为自适应图标:108dp 画布、中心 72dp 安全区、图形收敛在 66dp 内,含 background / foreground / monochrome 三层(支持 Android 13+ 主题图标)。笔画刻意加粗以适应 24–48dp 的单色渲染;原有位图保留作 API 26 以下回落。

英雄卡徽章改显示当前编解码器

  • 由固定的 API 级别改为显示耳机当前在用的编解码器(LDAC / AAC / SBC / aptX …)。该信息只有蓝牙栈掌握(BluetoothA2dp.getCodecStatus 为 @SystemApi、需 BLUETOOTH_PRIVILEGED),故经已有 Root 通道读取并解析 dumpsys,不维护任何编解码名称表;读不到时退回链路类型(LE Audio / ACL / LE),再退回「未连接」。异步查询 + 5s 节流,不阻塞 UI。

自定义映射交互规范化

  • 降噪 / 增益的裸数字输入框改下拉选择:可选值收敛为固定集合,越界与半截输入不再可能出现;降噪编辑仍四档全量落库。增益第 0 项为「隐藏该档位」(设备码 -1)。
  • 空间音频追踪标签由「每次按键都落库」改为失焦 / 回车才保存,空值视为恢复默认。
  • 「当前型号档案」与「当前:…」两行统一规格(12sp medium / primary)。
  • 修复菜单选中态不跟随:原先把创建时的选中值闭包进菜单,选完重开仍是旧高亮;改为行内维护可变选中态。

其他

  • 主界面「刷新状态」新增线性进度条(BLE 重连需数秒),状态回包即收起,6s 超时兜底。
  • 本轮另含(commit 749f8a6):未收录型号门禁指纹化(issue #5)、主界面电量行未连接仍显示、刷新按钮重试探测。
  • 版本号升至 alpha2.52(versionCode 287)

Assets

1

9/14/2026, 11:35:21 AM

正式发布 286-alpha2.51

  • 提交:7f1a653f400a3bc68896bd52223b2c740303dbe9
  • 触发:tag 286-alpha2.51
  • 构建方式:Release(作用域文件 META-INF/xposed/* 由 AGP 打包时合并)
  • 签名:CN=FxxkMoondrop(APK Signature Scheme v2)

更新日志

自上个正式版 285-2.50 以来的全部变更。

alpha2.51 (286)

修复 ANC 按钮映射「透传 ↔ 抗风」互换(issue #1)

  • 根因:设置页编辑任一档位时只写被编辑的那一格,其余格在读取时回退到名义默认映射 [1,2,3,4](AncProfileLib.DEFAULT_MAP),而不是该型号的档案映射。设备码 3 与 4 在不同型号上分别对应「抗风」与「透传」,名义顺序恰好与型号档案相反 —— 于是用户在 GA2 / 太空漫游2 上只要动过任意一格映射,两个按钮就静默互换。
  • 修复:GaiaBleClient.readAncMap() 的回退基准由名义默认映射改为型号档案映射(AncProfileLib.resolve(deviceName, null));并抽出纯函数 AncProfileLib.isStalePartialCustom(rebuilt, profileMap) 统一判定,消除内联重复。
  • 存量数据自愈:新增一次性 healStaleCustomAncMap(),识别旧版本部分写入留下的脏配置 —— 判定特征为「重建出的映射恰好 == 名义默认映射,而当前型号档案 != 名义默认映射」。命中则清除该自定义映射交回型号档案(标记 anc_map_migrated_v3,只跑一次);档案与名义顺序一致的型号恒不命中,绝不误清用户设置。
  • 源头堵住:设置页编辑任一档位改为四档按当前生效映射全量落库,不再产生「部分自定义」;提示文案补充当前生效的型号档案名(中英同步)。
  • 回归测试:新增 AncProfileLibTest(6 例)覆盖档案解析优先级、残留判定正例与两条反向护栏;期望值全部由公开常量与函数推导,不写死映射字面量。全仓单测 26 例全绿。
  • 无行为改动:未触碰连接链、协议识别、LE 地址发现、电量、DC 扩展控制、弹窗逻辑;readAncGetMap() 与 GET/SET 双向映射语义不变。已验证未自定义、四格全有效自定义两种场景与修复前逐字等价。
  • 版本号升至 alpha2.51(versionCode 286)

Assets

1

9/13/2026, 10:25:40 AM

正式发布 284-alpha2.41.10

  • 提交:a2591c5bead8455998757a7d91b2e4c446bd93cb
  • 触发:tag 284-alpha2.41.10
  • 构建方式:Release + EDF 注入(LSPosed 推荐作用域)
  • 签名:CN=FxxkMoondrop(v2/v3)

更新日志

自上个正式版 280-alpha2.41.6 以来的全部变更。

alpha2.41.10 (284)

监听开关合并:主界面按钮并入设置,单一入口

  • 主界面「开始/停止后台监听」按钮移除:该按钮与设置页开关职责重叠(一个管立即启停、一个管启动时自动监听),统一收敛到 设置 → 行为 → 后台监听,避免两处入口互相打架。
  • 设置「启动自动监听」升级为「后台监听」总开关:开启 = 立即开始监听,并把 enable / auto_service 双写为真(启动应用自动恢复、开机自启、后台防杀随之生效);关闭 = 停止监听服务、取消保活、双写为假。因原开关只管「启动应用时是否自动监听」、无法表达「立即启停」,与主界面按钮功能并不等价,故按合并处理而非直接删除。
  • 主界面保留英雄卡运行状态(运行中 / 未运行)实时展示,状态可见性不变。
  • SettingsActivity(遗留页)同步为总开关语义,避免代码库残留旧行为。
  • 版本号升至 alpha2.41.10(versionCode 284)

alpha2.41.9 (283)

issue #3 三连修:后台隐藏误退出 / GAIA 地址缓存污染 / 后台弹窗失效

  • ①「权限检查导致应用退出」(根因定位):MainActivity.onStop() 里无条件 finishAndRemoveTask()——bg_hide 打开后,只要离开主界面(进入「权限检测」二级页、跳系统授权页)就会销毁整个任务,用户回来时应用「自己退出了」。现改为只有 onUserLeaveHint(Home / 最近任务等用户主动离开)才隐藏,并用 Application.registerActivityLifecycleCallbacks 维护全局可见界面计数:还有其他本应用界面可见时不隐藏;配置变更(旋转/主题)直接跳过;隐藏前再延迟 500ms 复核一次,避免快速返回被误杀。应用内跳转与授权流程完全不受影响。
  • ②「能连耳机但所有功能失效」:gaia_le_addr 持久缓存污染链(坏地址写入后永不自愈),三处修复:
    • FastPairHookEntry:ACL_CONNECTED 推送地址时只校验 MAC 格式,不校验设备身份,任何蓝牙设备(键鼠/车机/其他耳机)连接都会把地址推给应用并落盘。现按真实设备名经 DeviceMatcher.isMoondrop() 过滤(不硬编码型号/MAC)。
    • GaiaBleClient:广播来源、LE 扫描命中、doConnectLe() 三处未经验证即写持久缓存(prefs + gaia_le_addr.txt)已全部改为只驻内存候选;只有 onServicesDiscovered 确认存在 GAIA 或 9ECA0000 服务后才 cacheVerifiedLeAddr() 落盘。
    • 服务发现确认该地址无 GAIA/9ECA 服务时,把地址加入 invalidLeAddrs 移出候选,并 clearLeAddrCache() 清除被污染的 prefs + 文件缓存,下次连接重新发现正确地址(自愈)。
  • ③「后台连耳机不弹窗」:与 ② 同源——GAIA 永远不就绪导致 PopupGate.flushPendingIfReady() 拿不到电量;叠加 bg_hide 误销毁任务,应用无法后台驻留。①② 修复后链路自愈。
  • 设置页「后台隐藏」文案同步澄清为「用户切到后台时隐藏主界面(不驻留最近任务);应用内跳转与授权流程不受影响」,避免与后台监听/弹窗功能互相误解。
  • 版本号升至 alpha2.41.9(versionCode 283)

alpha2.41.8 (282)

图标避开电量改为布局完成后轮询定位

  • 图标避让电量 subhead 的 top 偏移由「一次性计算」改为轮询等待 subhead 布局完成后再定位,避免 subhead 尚未测量时偏移量取 0、图标仍被电量百分比遮挡;新增详细定位日志便于排查。
  • 版本号升至 alpha2.41.8(versionCode 282)

alpha2.41.7 (281)

图标 top 动态避开电量 subhead

  • 设备详情/概览卡图标 top 由固定值改为动态计算,避开电量 subhead 区域,防止图标遮挡电量百分比。
  • 版本号升至 alpha2.41.7(versionCode 281)

Assets

1

285-2.50

Stable

9/13/2026, 4:30:04 PM

正式发布 285-2.50

  • 提交:6aef87d9d7e954be4c9b126ebcebda261d836270
  • 触发:tag 285-2.50
  • 构建方式:Release + EDF 注入(LSPosed 推荐作用域)
  • 签名:CN=FxxkMoondrop(v2/v3)

更新日志

自上个正式版 284-alpha2.41.10 以来的全部变更。

2.50 (285)

引入 DexKit 特征定位:GMS Fast Pair 混淆类名失效时自动兜底

  • 新增 DexKitLocator:用 dex 内的稳定特征串(日志格式串)反查被 R8 混淆的类,针对 Fast Pair 弹窗那几个只剩两三个字母的短类名(dtes / dthi / dtok)——它们随上游每次重编译都可能改名,硬编码类名迟早失效。
  • 改动范围仅 3 处 hook 点(FastPairHookEntry 的 dtes.f、dthi.O、dthi.q):把 Class.forName(x) 换成 DexKitLocator.resolveOrFallback(cl, key, x)。作用域(com.android.settings + com.google.android.gms)与包名不变。
  • 快路径零开销:先按原硬编码类名加载(与改造前逐字等价),只有加载失败(上游改版改名)才启用 DexKit 按特征定位;单进程单 bridge 缓存复用,定位耗时约 200–460ms,仅发生在兜底时。
  • 只增强、不替代:定位失败统一回退原硬编码名,并把原始异常原样抛出——任何机型 / 版本都不会因为定位器失效而丢功能。
  • dtok 的特殊处理:该类内部几乎全是 protobuf 字段、字符串常量只有单字母枚举名,没有可用特征串,改为「由持有它的 dtes 字段 c 的类型反推」——字段 c:Ldtok; 本就是现有 hook 依赖的成员,零新增风险。
  • 依赖与体积:引入 org.luckypray:dexkit:2.2.0;APK 内置 libdexkit.so。同时用 ndk.abiFilters 限定 arm64-v8a / armeabi-v7a,剔除只服务模拟器的 x86 / x86_64,单 APK 减约 0.8MB。
  • 实机验证:把硬编码名临时改成假名(dtes__FAKE_TEST 等)强制走兜底路径,DexKit 正确反查回真实类 dtes / dthi / dtok,GMS 三进程 hook 全部成功,证明兜底链真实可用。
  • 无行为改动:未触碰任何 hook 逻辑、GAIA / 9ECA 协议、设备库、弹窗逻辑;hookMoondrop / hookBluetooth 两条链保持原样(当前不在启用作用域内,代码保留以备多设备适配)。
  • 版本号升至 2.50(versionCode 285)

Assets

1

8/29/2026, 4:14:34 AM

FxxkMoondrop alpha2.38.9 (versionCode 269)

Moondrop 蓝牙耳机专用连接弹窗工具(LSPosed / libxposed API 102 模块)。

  • GAIA 协议直连控制(Antares/梦回2 Golden Ages 2)
  • 跨型号适配,不硬编码型号,按真实回包定路径
  • 开源 GPL-3.0

详见 CHANGELOG.md / https://github.com/bqj6666/FxxkMoondrop

Assets

1