一个 Android 充电记录应用。通过前台服务每秒采样电量,自动记录每次充电过程,将 0-100% 划分为 10 个 10% 档位统计每档耗时,使用密集窗口过滤离群值,预测充满时间。
- 自动记录:检测到充电自动开始记录,拔出充电器或充到 100% 自动结束
- 分段统计:将 0-100% 分为 10 档(每 10%),统计每档平均耗时
- 充满预测:基于历史平均数据,实时估算当前电量到 100% 的剩余时间
- 两种查看模式:「平均」展示历史均值,「上次」展示最近一次充电数据
- 常驻通知:通知栏显示当前电量、充电进度、已用时长、预计充满时间
- 开机自启:支持开机和应用更新后自动启动监听服务
- 异常过滤:密集窗口算法自动过滤边充边玩、换用慢充等异常数据
| 技术 | 用途 |
|---|---|
| Kotlin | 主语言 |
| Jetpack Compose | 声明式 UI |
| Room | 本地数据库 |
| Foreground Service | 后台持续监听 |
| KSP | Room 注解处理 |
| Material 3 | UI 组件 |
- Android 8.0 (API 26) 及以上
- targetSdk 35
# 克隆仓库
git clone https://github.com/ymlyyy/ChargeRecord.git
cd ChargeRecord
# 构建 Debug APK
./gradlew assembleDebug
# 构建 Release APK
./gradlew assembleReleaseapp/src/main/java/com/mulin/chargerecord/
├── MainActivity.kt # 主界面 + 前台服务 + 充电记录逻辑 + 充满预测
└── ChargeDb.kt # Room 数据库 + Repository + 过滤与平均算法
前台服务 ChargeRecordService 每秒读取一次电池状态:
- 通过
BatteryManager.BATTERY_PROPERTY_CAPACITY获取当前电量百分比 - 通过
ACTION_BATTERY_CHANGED广播判断是否正在充电
ChargeRuntime 维护一个状态机管理充电会话的生命周期:
- 开始:检测到充电开始(且电量 < 100%)时,记录起始时间和电量,开始收集数据点
- 记录中:电量百分比发生变化时追加
(percent, timestamp)数据点(百分比未变则不重复记录) - 结束:充电停止(拔出充电器)或电量达到 100% 时结束,触发分段计算和存储
- 丢弃:如果结束时电量没有增长,不保存本次记录
充电结束后,将收集到的数据点序列拆分为 10% 的档位(0-10%, 10-20%, ..., 90-100%)。
遍历相邻数据点对 (a, b),按时间比例将耗时分配到各档位:
电量从 35% 充到 42%,耗时 420 秒
→ 30-40% 档:分配 5/7 × 420 = 300 秒(观测跨度 5%)
→ 40-50% 档:分配 2/7 × 420 = 120 秒(观测跨度 2%)
跨多个档位的数据点会用游标逐档拆分,同一档位的多次贡献累加。
大跨度过滤:相邻数据点电量差超过 3% 时跳过,避免后台采样中断(如系统休眠)导致的虚假大跨度跳变污染统计。
每个档位存储两组时间:
observedSeconds:该档位内实际观测到的充电秒数observedSpan:该档位内实际覆盖的电量百分比
写入数据库前归一化到完整的 10% 跨度:
normalizedSeconds = observedSeconds × 10 / observedSpan
例如:30-40% 档只观测到 5% 的跨度(35%→40%),耗时 300 秒,归一化后为 600 秒,代表该档充满 10% 的等效耗时。
查询每个档位最近 50 条归一化样本后,使用密集窗口算法过滤异常值:
- 将样本按升序排序
- 用滑动窗口(宽度 = 3 分钟 = 180 秒)遍历,找到包含样本数最多的连续区间
- 仅保留该区间内的样本
这个设计针对使用同一充电器的场景:正常充电的每档耗时会稳定聚集,而边充边玩、临时换慢充头、系统休眠中断等导致的异常样本会偏离主簇被过滤掉。
样本数 ≤ 2 时不过滤,直接返回全部。
对过滤后的样本求简单算术平均:
平均耗时 = 样本之和 / 样本数
样本为空时返回 -1(UI 显示 "NA")。
基于「平均」模式的历史均值,计算当前电量到 100% 的预计剩余时间:
- 找到当前电量所在的 10% 档位
- 当前档位按剩余比例折算(例如电量 67%,该档均值 600 秒,则剩余 3/10 × 600 = 180 秒)
- 加上后续所有档位的完整均值
任意一个档位没有有效数据时返回 -1(UI 显示 "NA")。
将 10 个档位的均值直接相加,任意档位缺失则返回 -1。
Room 数据库 charge_log.db,两张表:
| 表 | 用途 | 主要字段 |
|---|---|---|
charge_session |
充电会话 | startTime, endTime, startBattery, endBattery, durationSeconds, segmentCount |
charge_segment |
分段样本 | sessionId, binStart, binEnd, observedSpan, observedSeconds, normalizedSeconds, createdAt |
查询规则:
- 每个档位查询最近 50 条样本(按 createdAt 降序),用于密集窗口过滤后计算均值
- 最近记录显示最近 10 次充电会话
- 「上次」模式读取最新一次会话的所有分段数据
| 权限 | 用途 |
|---|---|
POST_NOTIFICATIONS |
显示常驻通知(Android 13+) |
RECEIVE_BOOT_COMPLETED |
开机自启 |
FOREGROUND_SERVICE |
前台服务 |
WAKE_LOCK |
保持 CPU 唤醒 |
MIT