MoonEar

MoonEar(前身 FxxkMoondrop):Moondrop 蓝牙耳机助手(LSPosed / Xposed 模块)。耳机连接时自动弹出 Fast Pair 卡片,通过 GAIA BLE 协议直连耳机读取电量、控制降噪;3.3.0 起支持 5 段参数均衡(经典蓝牙 SPP 读写曲线与预设),3.3.1 起支持本地自定义预设与撤销更改,并新增调试模式(调试日志 / 服务探测)。最低系统要求 Android 12L。

作者:bqj6666 | 版本:3.3.1(versionCode 351) | 许可证: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 连接)
  • 设备通知:耳机电量与降噪控制合并为一条常驻通知(只显示左右耳,不含充电盒);档位按钮带大图标、当前档位高亮、颜色随系统动态取色,按钮按设备实际支持的能力生成
  • 快捷设置磁贴:下拉面板一键循环降噪档位,档位与通知栏 / 主界面共用同一份能力判据
  • 均衡器:预设可切换 + 参量均衡只读(仅在耳机提供 9ECA 服务时出现)
  • 弹窗时机:等就绪 / 延迟 0-5 秒 / 锁屏不弹 / 横屏不弹
  • 无 Root 模式:未检测到 Root 时自动回落为仅通过通知栏与 App 主界面控制降噪(GAIA BLE 直连本就不需要 Root),所有 Root 依赖项静默停用,不报错、不弹窗
  • M3 界面:主页 / 设置 / 关于,Material 3 + 动态取色
  • 设置页功能开关:官方集成(官方降噪面板 / 蓝牙详情页面板)、通知(电量通知 / 通知内降噪控制)可单独停用
  • 权限检测(整页二级界面):6 项实时检查,按必要权限(蓝牙 / 通知 / GAIA 直连)与可选权限(电池白名单 / Root / FastPairHook 模块)分组呈现,一键跳转修复;结论只看必要项
  • 日志抓取(设备适配):一键收集系统信息 / 应用设置 / 蓝牙 / 运行环境 / logcat 六类日志打包为 ZIP
  • 后台监听默认开启且无需 Root:开机及耳机连接时启动,无耳机 30 秒后停止服务和看门狗;仅保留非唤醒恢复,不再通过 Root 修改电池白名单或 appops。后台恢复受系统限制,电池优化白名单由用户授权。另有后台隐藏可选开关
  • 使用引导:首次启动自动展示,横滑分页、每页一个功能分区(关于 / 权限申请 / 连接管理 / 系统集成 / 耳机控制 / 适配与诊断 / 欢迎使用);页内开关与设置页同源,改完立即生效;看完或跳过后不再自动弹出,设置页最底部可随时重看
  • 显示层中英文切换:语言偏好(跟随系统 / 中文 / 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 直连),仅官方面板注入、弹窗图标自定义等增强项停用

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

351-3.3.1

Stable

11/2/2026, 8:13:24 AM

正式发布 351-3.3.1

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

更新日志

自上个正式版 350-3.3.0 以来的全部变更。

3.3.1 (351)

安全与稳定性优先的一版:hook ↔ App 的通信改成 UID 鉴权,蓝牙链路改成串行队列;
均衡器补上本地预设与「撤销更改」。

安全

  • hook 与 App 之间的 IPC、PrefsProvider、Slice 调用统一改为按已安装宿主 UID 鉴权(不再只看包名),并加自定义权限与 RECEIVER_NOT_EXPORTED 广播

蓝牙与连接

  • GATT 描述符写入/特征读取、GAIA 与 SPP 写入改为串行队列 + 回调驱动归属与超时,修复并发写导致的错乱与丢包
  • 会话退役与陈旧回调处理;部分/畸形/迟到回复的边界校验,分页有界
  • 耳机图标改为原子导入,避免半写文件

均衡器

  • 本地自定义预设:把当前曲线存成命名快照,支持重命名 / 删除 / 一键写入耳机(不占耳机里的三个预设槽位)
  • 撤销更改:未点「应用」前,可一键回到读入或上次写入回读的曲线

界面

  • 预设菜单 / 删除确认 / 命名弹窗统一为 M3 弹窗样式(28dp 圆角、56dp 列表行、删除用错误色、整行 ripple)
  • 弹窗卡片去掉 elevation,修掉圆角外侧两块直角灰块

修复

  • 「更新日志」读不到内容:缓存里存过只有版本名的残片,且「缓存优先」没有校验;改为缓存键升级 + 长度校验 + APK 内置 CHANGELOG 兜底
  • 半屏档位按钮第二次选中变回圆形:改用 GradientDrawable 直接绘形

Assets

1

350-3.3.0

Stable

10/23/2026, 4:18:02 PM

正式发布 350-3.3.0

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

更新日志

自上个正式版 340-3.2.20 以来的全部变更。

3.3.0 (350)

本版是大版本:应用更名为 MoonEar(仓库名沿用),最低系统要求提升到 Android 12L,
均衡器从「只读视图」重写成可读可写的参数均衡,并补上调试模式。

更名与基线

  • 应用名 / 关于页 / 桌面图标名统一为 MoonEar;包名保持不变(避免丢失作用域与用户设置)
  • minSdk 26 → 32(Android 12L 及以上)
  • 模块作用域收紧:移除官方 Moondrop App(EQ 已走 SPP,不再需要 hook 官方进程)
  • 图标重绘:耳机轮廓 + 三段均衡柱(沿用旧图标语言),含 monochrome 层支持系统主题图标

均衡器(重写)

  • 走经典蓝牙 SPP:0A 05 读曲线、0A 06 写曲线、0A 01/0A 02 读预设表与当前档、0A 03 切档
  • 39 字节载荷布局实测破解(并与同族开源实现交叉核对),支持 5 段参数均衡:频点 / 增益 / Q / 预增益
  • 写完自动回读逐字节校验(设备对 0A 06 不回 ACK,只能以回读为准)
  • 段数由载荷长度反推,布局不符自动降级为只读,避免写坏其他型号;写入前按官方范围钳制参数
  • 界面按 Material 3 重排:大标题、M3 Slider、官方按钮/chip、频响预览、加载态与失败重试

调试模式(新增)

  • 设置 → 诊断 → 「调试模式」开关(默认关);关闭时隐藏服务探测与 EQ 通道入口
  • 打开后新增「调试日志」页:应用内 AppLog + 相关 logcat(含 SPP 收发帧)

界面与交互

  • 均衡器、调试日志、手动连接、连接日志、服务探测、EQ 通道统一大标题顶栏(返回键随栏收缩)
  • 概览页降噪/增益等图标按钮、均衡器按钮统一按压形状形变(M3 Expressive 语义)
  • Fast Pair 半屏的档位按钮改为官方 MaterialButton,配色与按压形变与 App 内一致
  • 设置项归类修正:调试模式并入诊断、弹窗图标移入连接弹窗

后台监听

  • 移除 Root 对 deviceidle 白名单和 appops 的静默增强;后台恢复继续受系统策略约束,不再自动覆盖用户后续选择。升级不会撤销此前已授予的电池豁免或 appops 状态。
  • ACL 连接仅在 Moondrop 名称匹配时唤醒后台监听;名称缺失时只用同地址的已绑定设备名补查,仍无法确定则忽略。未收录型号仍只通过已连接 A2DP/HEADSET profile 进入协议探测。

修复

  • 修复「应用/切换预设后闪退」(后台线程回调里直接操作 View,跨线程异常)
  • 修复 SPP 收包不分帧导致的曲线错位(设备会粘包/半包)
  • 半屏档位按钮选中态改为圆角方形(M3 Expressive toggle 语义)

Assets

1

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