Skip to content

ADR 0010:用推送叫醒手机接来电

This content is not available in your language yet.

  • 状态:已采纳(2026-10-12,OUTB-68)
  • 决策者:zhibo li
  • 延续:ADR 0007(服务端只转发密文)、ADR 0009(单实例)

来电原来只走 SSE:server 只把来电推给当时连着 /v1/stream 的设备。手机不在流上,就没有任何东西会叫醒它,比如 App 被划掉、手机休眠、换网络、Android 前台服务被系统停掉,或者退避等待(最长 30 秒)。等它重新连上,来电已经过了 2 分钟的响铃窗口,只能进未接(OUTB-68:一通来电晚了 3 分 20 秒才到手机)。

server 也不知道哪台设备收到了来电。连接还在不代表能收到,手机网络上半开的 TCP 连接要 70 秒读超时才会断。出了问题只能从 lastSeenAt 倒推。

Android 前台服务一直连着流,是在和系统的省电策略对着干。国产 ROM 上尤其不可靠,iOS 上做不到。打电话、发消息的 App 通行的做法,是由服务端用系统推送叫醒设备,设备醒来后自己去取内容。

国行三星(OUTB-68 用户的手机)没有对第三方开放的厂商推送通道。装了 Google Play 服务,并且能连上 mtalk.google.com 时,FCM 可以用。

  1. 来电时由 server 推送叫醒手机,先用 Firebase Cloud Messaging(Android)。按 PushSender 接口实现,以后的 APNs、厂商通道也按这个接口加。
  2. 推送里没有内容,只有 { type: "call", eventId, seq, receivedAt }。手机拿到后用自己的令牌调用 GET /v1/events/:id 取密文,在本机解密、响铃。推送通道看不到汇报,ADR 0007 不受影响。
  3. 用 outbox 保证不丢:来电写库的同一个事务里,给本账号每台登记了令牌的设备各写一条 push_jobs,提交后马上发。server 在提交和发送之间重启,启动后会补发。
  4. 回执决定要不要再推,不看连接在不在。设备知道这通来电后回写 call_receipts,经推送或事件流都算,响铃和进未接也都算。没有回执的设备每 10 秒再推一次,最多 3 次。来电被接起、结束,过了响铃窗口,或者设备被移除,就不再推。推送的 TTL 截到响铃窗口结束,FCM 不会再晚送。
  5. 令牌跟「后台来电」开关走:开着时 App 用 PUT /v1/devices/me/push 登记 FCM 令牌,关掉时用 DELETE 撤回。FCM 回复令牌已失效时,server 删掉这个令牌。
  6. 凭证不进仓库:server 的 OUTBRIEF_FCM_SERVICE_ACCOUNT 是服务账号密钥。Android 打包时用 OUTBRIEF_FIREBASE_CONFIG 指向 google-services.json。两个都不设就没有推送,这是部署时的选择。设了但读不出来,server 启动失败,不会悄悄关掉推送。
  7. 推送和回执的记录保留一天,之后删掉(PushService.prune)。
  • 手机只要能收到 FCM,App 被划掉、手机休眠时也会响铃。没有 Google Play 服务的手机收不到 FCM,仍然只能靠事件流接来电。
  • 前台服务「后台来电」暂时保留,和推送同时在用:同一通来电从两条路到达时,CallAlerts 只响一次。推送稳定后,再决定要不要删掉前台服务和那条常驻通知。
  • FCM 的发送权限绑定 Firebase 项目和 App 的包名,只有用同一个项目打包的 App 才能被这台 server 推送。自部署用户如果用官方安装包,以后需要一个官方推送网关来转发唤醒,网关只看到令牌、来电 id 和时间。这件事还没做。
  • 推送在进程内发送,依赖 ADR 0009 的单实例约束。以后改成多实例时,push_jobs 需要加行锁或分片再分发。