# 折腾的番茄钟 (TOMATO 860) · 开发者文档

> 复古钟控收音机(RADIOTONE PETRA 860)拟物风格的番茄钟 Android 应用。
> 核心卖点:**锁屏实况倒计时**、专注期间**分心解锁计数**、**桌面小组件**、日/夜/跟随系统三套皮肤。
> 一句话定位:一台"长得像复古收音机"的效率记录工具——记录每日番茄数、每个番茄期间分心了几次,并能在锁屏直接看倒计时。

- 包名:`com.tomato860.pomodoro`
- 语言/框架:Kotlin + Jetpack Compose(拟物 UI 全部 Canvas 手绘)+ Room + DataStore
- minSdk 30 · targetSdk 34 · compileSdk 36(为 Android 16 实况窗 API 而用 36)
- 目标场景:Android 12+,在国产深度定制系统(如 ColorOS)上对"后台保活 / 到点提醒"做了专门加固
- 仅供学习交流,**请勿用于商业用途**。反馈/更新:zheteng95.xyz

---

## 1. 架构总览

```
                 ┌──────────────────────── UI (Compose) ────────────────────────┐
                 │  TimerScreen   StatsScreen   SettingsScreen   PermissionScreen │
                 │       └──────────── 订阅 state(只读) / 发命令 ───────┐       │
                 └──────────────────────────────────────────────────────┼────────┘
                                                                         ▼
   桌面组件 ───────────────────────────►   PomodoroController(进程级单例)
   PetraWidgetProvider ── 命令 ────────►    ├─ engine: PomodoroEngine(唯一状态机)
        ▲  render()                         └─ init():把设置项同步进引擎
        │ 刷新                                            │ 命令只发"意图"
        │                                                ▼
   PomodoroService(前台服务,唯一写权威) ◄───────────────┘
        ├─ 每秒 tick 引擎 + 刷新锁屏通知/实况窗 + 刷新桌面组件
        ├─ UsageStats 回查解锁次数(分心数权威值,抗后台冻结)
        ├─ 阶段结束/分心提醒 → AlertPlayer;专注自动勿扰 → DndController
        └─ 会话落库(Room)+ 会话持久化(抗进程被杀)
```

**关键约束:状态只有一份。** `PomodoroEngine` 是唯一状态机;UI 与桌面组件都只**读** `PomodoroController.engine.state`(`StateFlow`)、只**发命令**;真正修改状态的唯一权威是 `PomodoroService`。这样避免"UI 直接改 + 服务也改"的双路径竞争,任何时刻状态自洽。

## 2. 模块清单

| 包 | 文件 | 职责 |
|---|---|---|
| (根) | `TomatoApp.kt` | Application:启动即 `PomodoroController.init()` |
| | `MainActivity.kt` | 单 Activity:ThemeMode → `TomatoTheme`、底栏切 3 屏 |
| `core` | `PomodoroEngine.kt` | 状态机:FOCUS/SHORT_BREAK/LONG_BREAK/IDLE 流转、倒计时、超时累加+封顶+解封、分心计数、事件流、快照恢复 |
| | `PomodoroPhase.kt` / `PomodoroConfig.kt` | 阶段枚举;番茄规则配置(时长/长休/提示音/震动/勿扰…) |
| `data` | `SettingsStore.kt` | DataStore 持久化配置 + 独立的外观 `ThemeMode` |
| | `StatsRepository` / `dao` / `entity` / `AppDatabase` | Room:专注/休息会话落库,按天聚合 `DailyStat`,支持任意区间查询 |
| `service` | `PomodoroController.kt` | 进程单例:持有 engine,对外命令(start/pause/skip/continueOvertime/abandon) |
| | `PomodoroService.kt` | 前台服务:tick、通知、组件刷新、解锁监听、落库、勿扰、保活、会话持久化 |
| | `PomodoroNotifications.kt` | 锁屏通知 + Android 16 实况窗(Live Updates)构建 |
| | `UnlockTracker.kt` / `ScreenUnlockReceiver.kt` | UsageStats 回查解锁数(抗冻结)+ 即时解锁广播 |
| | `AlertPlayer.kt` / `DndController.kt` / `PermissionCenter.kt` | 提示音/震动、系统勿扰联动、权限聚合检测+跳转 |
| | `KeepAlivePlayer.kt` | 无声音频保活(对抗深度定制系统熄屏冻结,见 §4.1) |
| | `PhaseAlarm.kt` / `PhaseAlarmReceiver.kt` | 到点 `setAlarmClock` 精确闹钟 + 接收器 |
| | `SessionStore.kt` | 持久化当前会话(进程被杀后冷启动恢复) |
| `ui/*` | `timer` / `stats` / `settings` / `permission` / `theme` / `components` | 计时主页(可旋转拨轮)、统计(月汇总+自定义区间)、设置、权限引导(首次安装向导:欢迎+进度+必需/可选分组;设置内为清单模式)、日夜调色板、复古组件 |
| `widget` | `WidgetRenderer.kt` / `PetraWidgetProvider.kt` | Canvas 渲染整台收音机为 Bitmap 作底图 + 倒计时叠加 + 按钮路由命令 |

## 3. 番茄循环逻辑

流转:`待机 ─开始→ 专注(倒计时) ─到点→ 专注超时 ─去休息→ 休息(倒计时) ─到点→ 休息超时 ─下一个番茄→ 专注 …`(每 `longBreakEvery` 个番茄,"去休息"自动走长休)。

- **到点不自动跳转,进入"超时"累加**(专注计专注时长、休息计休息时长),由用户主动推进。这点偏 Flowtime 而非"硬停"。
- **防跑飞**:超时自动累加**封顶 5 分钟**即冻结——"完成后走开不管"最多多记 5 分钟,不污染统计。要继续累加须主动点"继续专注 / 延长休息"解封,手动结束才停;解封专注累计满一个番茄时长则额外计一个番茄。
- 按键(完成/超时节点无"暂停"):倒计时 `[放弃|暂停/继续|跳过]`;专注超时 `[结束|去休息|继续专注]`;休息超时 `[结束|下一个番茄|延长休息]`;锁屏通知按钮同步。

## 4. 三大核心逻辑

应用最终收敛于把这三块做对,均在真机(国产深度定制系统)上验证。

### 4.1 计时
- **墙钟计时**:`remaining = endAtEpochMs - now()`,`endAtEpochMs` 是阶段开始时确定的**绝对时间戳**;进程被冻结/杀掉都不丢时间、到点目标不漂移。
- **到点**:用 `AlarmManager.setAlarmClock`(最高优先级,Doze 也投递)注册到绝对 `endAtEpochMs`;**即使进程被系统杀掉,也能在目标时刻冷启动服务并提醒**。
- 抗被杀:`SessionStore` 持久化会话(含绝对 `endAtEpochMs`),冷启动按绝对到点恢复;>6h 的旧会话视为废弃不复活。

### 4.2 通知
- **锁屏倒计时用系统 `Chronometer`**(`setUsesChronometer(true)` + `setChronometerCountDown(true)` + `setWhen(now+remaining)`):由系统 SystemUI 自走字,**进程被冻结也照样在锁屏倒数,不会暂停**。
- **到点提醒**:`MediaPlayer`(`USAGE_ALARM`,绕过勿扰)+ `Vibrator` 直驱硬件,**独立于通知渠道**(渠道即便被系统降级/禁用也照响照震);持续响到用户操作。
- **解锁即停**:监听 `ACTION_USER_PRESENT`,用户真正解锁即停止提醒;界面回到前台(已解锁)也停。

### 4.3 勿扰联动
- 规则:`勿扰开关 && 专注中 && 运行中 && 非超时` → 开启系统勿扰(仅优先级),其余一律关闭。
- **关勿扰时直接放行全部**(不"还原进入前的过滤级别")——否则进程在专注中被系统重启、把"勿扰"态误存成"原始值"时,会出现"暂停后勿扰怎么也关不掉"。
- **暂停立即关勿扰**,不依赖会与系统失同步的内部标志。

## 5. 实现要点 & 工程难点

### 5.1 深度定制系统的后台冻结(到点不响)
部分国产系统会对侧载应用激进冻结:熄屏数秒就冻结整个进程,前台服务 + `WakeLock` + 电池白名单都不一定拦得住。
- **缓解**:计时期间用 `MediaPlayer` 循环播放一段**极低振幅、不可闻的非零波形音频**(音量给小的非零值,不能 0——全静音会被判定"没真在播"照冻),`USAGE_MEDIA` 不抢音频焦点。系统认其"真在播放媒体"就倾向不冻进程。配 `setWakeMode(PARTIAL_WAKE_LOCK)` 防深睡 CPU 休眠导致音频中断。前台服务类型须含 `mediaPlayback`。
- **可靠兜底**:即便保活在纯电池下仍被杀,§4.1 的 `setAlarmClock` 也能准时把进程冷启动起来提醒——这是到点提醒的最终保障。

### 5.2 分心解锁计数(抗冻结)
- 监听 `ACTION_USER_PRESENT` 广播在被冻结时收不到。故以 **`UsageStatsManager` 回查**专注窗口内的 KEYGUARD_HIDDEN 次数为权威值,广播仅作前台即时反馈;两路去重。需"使用情况访问"权限。
- 暂停期间的解锁不计:检测暂停/恢复边沿,把暂停区间内的解锁从权威值里扣除。

### 5.3 锁屏实况倒计时(Android 16 Live Updates)
- `NotificationCompat`:`setRequestPromotedOngoing` + `setShortCriticalText` + `ProgressStyle`,把常驻通知升格为锁屏实况窗 + 状态栏灵动胶囊(API ≥ 36,依赖 androidx.core 1.17.0)。
- 系统模板渲染,**禁止自定义 RemoteViews**——全拟物 UI 只在应用内;锁屏只能调强调色/进度条/图标/文字。
- 系统 `Chronometer` 一开就独占折叠胶囊:有分心时改用分钟级文本 + 每 3 秒交替"分心 N / 剩余",秒级与交替不可兼得。

### 5.4 桌面组件的拟物质感
- AppWidget 的 RemoteViews 不能用 Canvas/Compose,扁平 shape 撑不起质感。
- 解法:`WidgetRenderer` 用 **Canvas 把整台收音机画成高清 Bitmap** 作底图,上层只叠加自走字的 `Chronometer`(秒级倒计时)+ 透明点击区(`PendingIntent` 路由回 Provider → 命令)。
- 防重叠:每个圆/按钮半径取 `min(分区宽度, 竖向半高)` 双重约束,任意宽高比都不越界。

### 5.5 已知平台限制:锁屏"自动亮屏"
"到点自动点亮屏幕弹整页提醒"依赖 `USE_FULL_SCREEN_INTENT`,Android 14+ 默认拒绝该权限(全屏意图被降级为普通通知),且部分系统对其有额外限制、`SCREEN_BRIGHT_WAKE_LOCK` 也被弱化。**本应用不强依赖亮屏**:到点的响铃 + 震动直驱硬件、独立于通知与亮屏,已足够提醒;"自动亮屏"作为可选项(用户手动授予全屏通知权限后生效)。

## 6. 主题(日/夜/跟随系统)
- `ThemeMode { SYSTEM, LIGHT, DARK }` 存于 DataStore(与番茄配置解耦)。
- `MainActivity` 订阅后解析明暗传给 `TomatoTheme(darkTheme=)`,并同步系统栏图标明暗;设置页「外观」分段控件即时切换。
- 夜间不是简单反色,而是换"墨黑胶木 + 琥珀背光表盘"材质。

## 7. 构建

标准 Gradle 工程,JDK 17:

```bash
./build.sh assembleDebug      # 调试包
./build.sh assembleRelease    # 发布包(R8 minify + shrinkResources,arm64-v8a,约 2.9MB)
```

- Release:`isMinifyEnabled` + `isShrinkResources` + `abiFilters arm64-v8a`;关 R8 fullMode + keep `androidx.lifecycle`/`compose` 规则,修 "LocalLifecycleOwner not present" 崩溃。
- 锁屏实况窗、保活等行为**无法在模拟器验证**,需真机(国产深度定制系统)确认。

## 8. 运行所需权限(侧载需逐项手动开,设置页"权限中心"有检测+跳转)

| 权限 | 用途 | 关键性 |
|---|---|---|
| 通知(POST_NOTIFICATIONS) | 锁屏实况窗、提醒 | 必需 |
| 使用情况访问(PACKAGE_USAGE_STATS) | 分心解锁计数(抗冻结) | 必需 |
| 闹钟和提醒(USE/SCHEDULE_EXACT_ALARM) | 到点精确唤醒 | 必需 |
| 全屏通知(USE_FULL_SCREEN_INTENT) | 到点自动点亮屏幕 | 可选(不开也照样响铃震动) |
| 勿扰访问(ACCESS_NOTIFICATION_POLICY) | 专注自动勿扰 | 可选 |
| 电池不优化 + 系统"自启动/允许后台" | 防后台杀 | 强烈建议 |
