Android BLE 心率监测应用(纯中文 UI)。2026-07 完成大规模重构,架构约定如下,改动时必须遵守。
- 编译环境在 distrobox
dev容器:distrobox enter dev -- ./gradlew :app:assembleDebug - 工具链:JDK 25 / Gradle 9.6.1 / AGP 9.3.1(内置 Kotlin,不要添加 org.jetbrains.kotlin.android 插件)/ KSP / Room Gradle Plugin
- compileSdk 37(minor 1,见 app/build.gradle.kts 的 compileSdk DSL 块),targetSdk 36,minSdk 27
- 签名:keystore.properties(gitignored)或环境变量 KEYSTORE_FILE/KEYSTORE_PASSWORD/KEY_ALIAS/KEY_PASSWORD;没有则 release 回退 debug 签名
- 依赖一律进 gradle/libs.versions.toml,禁止在 build.gradle.kts 内联版本号
- 单元测试:
./gradlew :app:testDebugUnitTest
core/AppContainer:手动 DI 组合根(无 Hilt/Koin)。进程级单例只能在这里创建,组件经AppContainer.get(context)取用。domain/HeartRateRepository:心率/连接状态的唯一事实来源(StateFlow)。UI、Service、磁贴全部从这里取数。禁止 bindService/Binder 传数据。data/settings/SettingsRepository:唯一的设置入口(Preferences DataStore,键定义在 SettingsKeys,默认值唯一定义在 AppSettings)。禁止直接 getSharedPreferences;变更监听用settings.flowOf { ... }。ble/BleConnectionManager:扫描/连接/心率订阅/指数退避自动重连(5s→10s→30s→60s 封顶,无限重试)。service/:全部是"系统集成壳"(前台保活、通知、窗口),不承载业务逻辑。BleService 生命周期控制 ServerController(HTTP/WS 服务器)。- BleState 是纯状态(无 UI 文案);文案映射唯一入口
ble/BleStateTexts(UI 与 HTTP/WS 接口共用)。 - UI 层:StateFlow +
repeatOnLifecycle(STARTED)收集;一次性事件用 SharedFlow/Channel;禁止 LiveData。
本项目定位家庭局域网游戏生态,与桌面版 HeartRateMonitor、HeartRateWidget 互通,兼容优先于安全:
- 签名密钥内置于仓库
.key/key(维护者决策):任何人克隆编译出的 APK 与官方签名一致, 社区构建可覆盖安装官方版本。keystore.properties/环境变量可覆盖。 - 内置 HTTP/WS 服务器绑定所有网卡(HeartRateWidget/桌面版无认证能力,需免配置直连); Token 认证为可选项(server_auth_required,默认 false)。
- Webhook 允许 http 与 https(官方预设、VRChat OSC、sleepy-project 全是 http); manifest 已开 usesCleartextTraffic。
- 仍然保留的防线:GitHub 预设同步走预览+确认+强制 enabled=false;云备份排除健康数据与凭据。
- 设置开关 comparison_mode_enabled(默认关)门控全部对比 UI;关闭时主页与 v2.0 行为一致
ble/ComparisonDeviceManager:对比设备连接层,独立于主引擎(无 Webhook/历史副作用、独立扫描不打断主连接)- 指标计算在
domain/DeviceMetrics.kt(RollingRate/BpmDiffAccumulator)与domain/AccuracyReport.kt(历史回放 MAE/最大差),全部纯 Kotlin 有单测 - 图表唯一实现
ui/chart/HeartRateChartController(多序列/主题化/绝对时间轴),主页与历史详情共用;新增序列配色须走 colorForIndex - 数据库 v3:RecordingSession → SessionDevice → HeartRateRecord(含 rr 列);迁移 2→3 已是正式 Migration,禁止再引入 fallbackToDestructiveMigration
- HTTP/WS JSON:既有字段=主设备(生态契约),多设备走增量
devices数组
设置 keep_alive_channel:FOREGROUND(默认)| ACCESSIBILITY,用户在设置页显式选择。
service/overlay/FloatingWindowHost:悬浮窗唯一实现,windowType 由持有者注入; FloatingWindowService(TYPE_APPLICATION_OVERLAY)与 HeartRateAccessibilityService (TYPE_ACCESSIBILITY_OVERLAY)共用,禁止再在服务里复制窗口逻辑core/OverlayCoordinator:登记无障碍是否生效 + 下拉面板是否展开 + 触摸穿透动作路由; 磁贴/主页/状态栏据此分支,新增依赖通道的行为一律读它,不要各自判断HeartRateAccessibilityService能力必须保持最小:canRetrieveWindowContent=false、 仅 typeWindowStateChanged(用于下拉面板让位);不得为任何功能扩大无障碍权限- accessibility overlay 层级高于状态栏与下拉面板 → 状态栏常驻在面板展开时临时隐藏
- 触摸穿透通知按钮走
FloatingWindowActionReceiver广播(跨通道路由),不要改回 startService - 通道属主判断一律用
OverlayCoordinator.isAccessibilityChannel()(= 系统已开启 AND 用户选了该通道); 只看accessibilityActive会在"切回前台通道但没关无障碍"时误判服务器归属 - 冷启动补启:ContentProvider 早于无障碍连接,其判断只能读设置;状态栏/预警的补启在
HeartRateAccessibilityService.onServiceConnected内完成 - 无障碍被系统关闭时必须回退前台通道(teardown → 写回设置 + 拉起前台服务),否则全应用静默失效
- 定向自动连接/重连使用
Filter.Address过滤扫描(息屏下无过滤扫描不投递结果);手动扫描保持无过滤 - 无障碍通道 = 全应用零常驻通知:BleService 不启动(MainActivity/磁贴/预警三处调用点均已判通道),
StatusBarResidentService 与 HeartRateAlarmService 的 ensureResidentForeground 直接返回并撤掉已有通知。
新增任何
startForeground/startForegroundService调用点都必须先判accessibilityActive - accessibility overlay 必须用无障碍服务的 WindowManager(携带 window token), 普通 Service 的 WM 会被 WMS 以 BadToken 静默拒绝——见 OverlayCoordinator.accessibilityContext
data/update/UpdateRepository + domain/VersionComparator(纯函数有单测)。
克制原则不得违反:只在 MainActivity 进入时触发(磁贴/后台启动绝不检查)、24h 节流、
预发布版不提示、用户可跳过某版本、任何失败静默忽略。
- Material 3 DayNight + 动态取色;色值/间距用主题 attr 与 @dimen(values/dimens.xml),不裸写
- 滑条用 Material Slider(真实值域写在布局 valueFrom/valueTo);返回箭头 @drawable/ic_arrow_back
- 心跳动画统一用 service/overlay/HeartbeatAnimator,不要再复制粘贴 ValueAnimator
- Kable 0.32 → 0.4x 升级(API 大改)作为独立任务
- 历史图表全量加载:长会话(数万行)一次性进内存喂 MPAndroidChart,可在 SQL 层按时间分桶抽稀
- RR 间期(0x2A37 帧内自带)当前在 parseHeartRate 被丢弃;将来做 HRV/压力分析时从解析层带出
- 心率消费约定:时序敏感的消费方(预警、图表、记录)必须收
heartRateSamples逐样本流;heartRateStateFlow(按值去重)只用于"当前值展示"(数字、悬浮窗、通知、磁贴) - WS 1Hz 恒频推送是对外契约(客户端靠持续推送判活),若改"变化即推+心跳帧"属版本化变更需谨慎
- 签名密钥:用户已决定沿用现有 key(保证老用户可覆盖安装)。密钥文件在本地
.key/keykeystore.properties(均已 gitignore)。注意:该 key 曾出现在公开 git 历史中, 风险已知晓并被接受;如日后要换 key,直接替换 keystore.properties 即可。
- i18n:中英双语已完成——默认英文
values/strings.xml,中文values-zh/strings.xml(跟随系统语言,第三方语言回落英文)。新增用户可见文案必须同时进两个 strings.xml (220 个 key,键集合必须保持一致)。数据层持久化字符串(如设备名回退"未知设备")不翻译。