最开始的问题很普通:在 APK 里设了待办和定时任务,到点后手机没有任何提醒。
任务其实已经保存到 mini-agent 了。以前的提醒出口是 Telegram;APK 只是一个能查看和创建任务的客户端。把“保存任务”和“在手机上提醒”混在一起想,方案会越做越重。后面这轮折腾,主要是在把两件事拆开。
一开始想走 FCM
第一个版本是标准的 FCM 路线。
- APK 接入 Firebase Messaging,登录后上报 FCM token;
- 后端保存 token;
- 定时任务触发时,后端用 Firebase Admin SDK 调 FCM HTTP v1;
- APK 收到消息后用
NotificationCompat展示系统通知。
这套东西在有 Google Play 服务、服务端能访问 Google 的环境里没有什么特别之处。Android 端还需要 POST_NOTIFICATIONS,Firebase 项目要有 google-services.json,服务端则要保存 Admin service account。私钥没有进 APK,也没有提交到 Git。
问题不在代码。
国内设备不一定有 GMS,即使有,FCM 也不稳定。更直接的一次检查是在阿里云 ECS 上:oauth2.googleapis.com:443 和 fcm.googleapis.com:443 都能解析 DNS,但 HTTPS 连接超时。没有 OAuth access token,FCM HTTP v1 根本发不出去。
所以 FCM 被保留为 Google 生态下的可选通道,但不能承担这台个人设备的提醒主链路。
厂商推送看起来对,实际卡在资质
接着调研了小米、华为、OPPO、vivo 等厂商通道,以及个推、极光这类聚合推送服务。
这确实是国内 App 的常规做法:厂商系统进程维持长连接,应用进程被杀掉后仍可能收到通知。定时提醒应该按“服务与通讯”“私信”“系统消息”之类的类别发送,不能落到营销通道。
但个人项目碰到了另一个边界。以小米通道为例,实际接入和消息类别申请需要企业开发者资质。为了一个个人 mini-agent 去接多厂商 SDK、控制台和合规披露,投入明显不成比例。
这条路没有继续推进。不是厂商通道不好,而是它不适合当前的约束。
也试过让本机替云端发 FCM
既然 ECS 到不了 Google,曾经考虑过另一条链:APK 把任务和 token 放到云端,本机 mini-agent 同步任务,到点后本机持有 Firebase Admin 凭据并发送 FCM。
它能绕过 ECS 的网络限制,但立刻带来一个新问题:本机什么时候能拿到刚在 APK 创建的任务?每日同步显然不够。长轮询、变更队列、版本游标、断线补偿都可以做,只是为了通知又引入了一条常驻同步链路。
而且本机必须开着。它从“推送服务端”变成了家里的提醒网关,个人使用可以接受,架构上却不够干净。
这次探索留下的结论是:任务数据可以同步,提醒不必一定通过远程推送完成。
最后把待办和定时拆开
最终方案没有继续追求“一种通知渠道解决所有问题”。待办和定时的语义不同,落到 Android 上也不该用同一种组件。
待办:写入系统日历
待办有截止时间时,APK 在云端创建成功后写入 CalendarContract:
- 创建一个系统日历事件;
- 添加提醒;
- 保存 mini-agent 任务与日历事件的映射;
- 待办完成后,先更新云端状态,再删除对应日程。
日历负责设备重启后的恢复、系统通知和用户可见的时间线。APK 需要 READ_CALENDAR 与 WRITE_CALENDAR 权限。
用户可以在日历里修改或删掉事件,但日历不是任务真相源。下一次 APK 刷新时,云端仍然是准的:该存在的事件会被补回,已完成任务的事件会被清掉。
定时:使用 APK 自己的 AlarmManager
定时任务不写日历。APK 用 AlarmManager 登记本地提醒,到点由 BroadcastReceiver 直接发系统通知。
一次性定时使用时间戳;每天、每周、每月这几种重复规则会在触发后计算下一次并重新登记。重启、系统时间变化和时区变化后,Receiver 会把已登记的任务恢复回来。
Android 12 以后,精确闹钟还涉及“闹钟和提醒”特殊权限。拿不到权限时应用会退回 setAndAllowWhileIdle(),提醒可能不再精确到分钟。这比静默失败好,也比假装能保证到达更诚实。
这里的“闹钟”是 APK 的本地提醒,不是把一条记录无声地塞进系统时钟 App。第三方应用不能这样做;若真要创建系统时钟的闹钟,只能跳转到时钟 App,由用户确认。
真相源不是一台机器,而是任务的 origin
后来还有一个更棘手的问题:Telegram、本机和 ECS 都能看到任务。如果每一端都可以修改同一条记录,日历、闹钟和 Telegram 很快就会重复提醒或互相删错。
现在把任务分成两个 origin:
| origin | 创建与修改 | 执行提醒 | APK 的本地投影 |
|---|---|---|---|
1 | APK、Web、ECS | APK | 待办进日历;定时进 AlarmManager |
0 | Telegram、本机 mini-agent | 本机 mini-agent | 只展示,不创建日历或闹钟 |
一条任务的身份是 (origin, id),不能只用 id。两个端都可能有 id=1,但它们不是同一条任务。
本机与 ECS 保留每日双向同步:同步的是两个 origin 的记录集合和状态,不改变 origin。ECS 上的 LOCAL_ORIGIN=1,本机默认 0。服务端也会拒绝显式跨 origin 的写请求。
这样做以后,APK/Web 创建的任务不会被 Telegram 端接管;Telegram 创建的任务也不会在手机上再响一遍。两个端都能看见任务全集,但各自只负责自己的分区。
APK 刷新时做 reconcile
没有 FCM,网页刚创建的任务不可能立刻出现在一台未打开的手机上。这是当前方案明确接受的边界。
补救不是在后台偷偷保活,而是让 APK 在用户进入“待办”或“定时”Tab、或手动刷新时从 ECS 拉取完整快照,然后做一次 reconcile:
- 读取 ECS 返回的任务集合;
- 忽略或只读展示
origin=0任务; - 为仍有效的
origin=1待办补建日历事件; - 为仍有效的
origin=1定时补建本地闹钟; - 清理任务已完成、已删除或一次性时间已过的本地投影。
网页直接创建的定时任务,仍需要 APK 在到点前至少打开一次对应 Tab 才能在手机上登记闹钟。这不是“实时推送”,但对个人工具来说,行为是可解释的:任务在云端保存,手机在刷新时把它落到本机系统能力里。
收尾
这次没有得到一个万能推送方案。
FCM 受 GMS 和服务端网络限制;厂商推送受资质和接入成本限制;让本机转发 FCM 又要维护实时同步。最后留下的是更贴近任务语义的组合:待办交给系统日历,定时交给本地闹钟,Telegram 保持本机任务的提醒出口。
它不是跨设备实时提醒系统。它是一个个人工具在现有网络和资质条件下,能稳定解释、能维护、也不需要藏着掖着的提醒方案。