2026-05-18 11:10:52 +08:00

11 KiB
Raw Blame History

Context

当前 app 已有短信验证码接收实现Manifest 声明了 RECEIVE_SMSREAD_SMS,静态 SmsReceiver 监听 android.provider.Telephony.SMS_RECEIVEDMainActivity 提供权限申请、最近结果展示、READ_SMS 最新短信读取、ContentObserver 和短时轮询诊断。用户反馈“逻辑试过了能用”,但不确定为什么某些场景 onReceive 收不到。

目标设备是小米 12S、澎湃 OS 3、Android 15。根据 Android 官方文档,RECEIVE_SMS 允许应用接收 SMS但它是 dangerous 且 hard restricted 权限,是否能真正持有可能受安装来源/安装器 allowlist 影响。Android 15 又新增了 BOOT_COMPLETED 启动部分前台服务类型的限制Doze/App Standby 白名单也不是无限制后台执行。小米官方支持文档说明 HyperOS/小米系统存在“Background autostart”用户开关路径为 Settings > Apps > Permissions > Background autostart。

参考资料:

Goals / Non-Goals

Goals:

  • 先形成完整 spec不直接写代码。
  • 在当前可用短信接收 app 上补齐后台保活和开机恢复方案。
  • 让用户能手动完成小米设置,并在 app 内看到哪些设置已完成、哪些只能人工确认。
  • onReceive 收不到时有明确诊断权限、安装来源、force-stop、系统广播、HyperOS 后台策略、短信是否进入收件箱。
  • 后续实现保持可解释、可关闭、可验证。

Non-Goals:

  • 不做无通知、不可感知、规避系统策略的隐藏保活。
  • 不承诺杀进程、force-stop、清后台、系统级省电清理后仍 100% 存活。
  • 不把 C++ fork/native daemon 当成可靠能力Android 应用沙箱和系统进程管理不会因为 native 代码而失效。
  • 不把 app 做成默认短信客户端。
  • 不要求本轮编译。

API Strategy

1. 系统短信广播仍是主路径

短信进入设备时,主路径仍是 SMS_RECEIVED_ACTION。这是最直接的验证码捕获路径,但它依赖:

  • 应用真正持有 RECEIVE_SMS
  • 应用未被用户 force-stop。
  • 系统或厂商策略允许静态 receiver 在当前状态下被拉起。
  • 短信确实由 Android Telephony 入库/分发,而不是被系统短信 app 或安全策略特殊处理。

现有 SmsReceiver 的 manifest 写法包含 android:permission="android.permission.BROADCAST_SMS",该写法用于限制只有持有系统广播权限的发送方能投递该 receiver系统短信广播通常满足此条件。后续诊断要验证它不是问题根因但不建议先移除。

2. 前台服务用于“可见后台运行”,不用于读取短信

新增 SmsKeepAliveService

  • app 前台点击“开启常驻保活”后调用 startForegroundService
  • 服务在 5 秒内调用 startForeground,展示低打扰常驻通知。
  • 通知内容显示“短信监听运行中”、最近一次心跳、最近一次短信来源。
  • 服务只做状态心跳、通知刷新、诊断状态保存。
  • 短信接收仍由 SmsReceiver 和收件箱兜底路径处理。

这样做的原因是前台服务可以提升后台进程可见性,但不能替代短信广播,也不能保证被厂商永不杀。它的价值是让系统和用户明确知道该 app 在后台运行,并给 HyperOS “省电无限制 + 自启动”一个更稳定的承载对象。

3. 开机自启动采用轻量 BootReceiver

新增 BootReceiver 监听:

  • android.intent.action.BOOT_COMPLETED
  • android.intent.action.LOCKED_BOOT_COMPLETED
  • android.intent.action.MY_PACKAGE_REPLACED

收到后:

  • 记录 boot 事件和时间。
  • 检查用户是否曾开启“常驻保活”。
  • 如果已开启,则尝试启动 SmsKeepAliveService
  • 捕获 ForegroundServiceStartNotAllowedException 等异常,写入诊断,不崩溃。

Android 15 对 BOOT_COMPLETED 启动前台服务的限制集中在 dataSync、camera、mediaPlayback、phoneCall、mediaProjection、microphone 等类型。当前保活服务不应声明这些类型;如果后续升级 targetSdk 到 35仍必须真机验证 BootReceiver 启动服务是否被 HyperOS 额外限制。

4. 电池优化与 HyperOS 设置采用“检测 + 引导”

Android 标准能力:

  • PowerManager.isIgnoringBatteryOptimizations(packageName) 展示是否在电池优化白名单。
  • 提供 Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS 打开白名单设置。
  • 个人自用场景可考虑 ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 直接请求,但 UI 必须说明它仍不等于无限后台。

小米/HyperOS 能力:

  • 提供“打开小米自启动设置”按钮,优先尝试常见 MIUI/HyperOS 安全中心组件。
  • 如果显式组件不可用,回退 ACTION_APPLICATION_DETAILS_SETTINGS 和通用系统设置。
  • UI 显示人工清单:
    • 权限:短信权限已授权。
    • 自启动:用户已在 HyperOS 开启。
    • 省电策略:用户已设为无限制。
    • 通知:常驻通知未被关闭。
    • 保活服务:正在运行。

自启动和省电无限制通常没有稳定公开 API 可直接读取真实状态,因此必须允许用户手动勾选“我已完成设置”,并把这部分标记为人工确认。

5. Native/C++ 方案只做实验诊断

可以加入一个可选 native heartbeat

  • Java 服务加载 native library。
  • native 层定期写入一个本地 heartbeat 文件或通过 JNI 回调返回时间戳。
  • 用于确认进程活着、JNI 正常、服务被杀后心跳停止。

不建议实现 native fork daemon

  • Android 应用进程被系统管理,子进程同属应用 UID/cgroup厂商清理通常会一起处理。
  • 后台私自 fork 常驻进程会带来电量、兼容性和安全风险。
  • 对短信广播恢复没有直接帮助。

如果用户坚持尝试,必须放在“实验开关”下,并以诊断结论交付,不作为保活主链路。

Why onReceive May Not Fire

后续 UI 和 logcat 诊断必须覆盖以下根因:

  • RECEIVE_SMS 未授权,或因 hard restricted 机制安装后实际不可持有。
  • app 被 force-stopAndroid 不会为 force-stopped app 投递大多数隐式广播,直到用户再次打开。
  • 小米自启动未开启,系统不允许后台拉起静态 receiver 或服务。
  • 省电策略不是无限制,后台/锁屏/待机时进程或广播处理被延迟/限制。
  • app 刚安装后从未启动,部分系统不会让 receiver 正常进入用户期望状态。
  • 短信由运营商/RCS/系统短信能力特殊处理,未走普通 SMS_RECEIVED。
  • 双卡或短信格式异常导致 PDU 解析失败,但此时 receiver 应该已有日志。
  • Manifest receiver 被禁用、包名不匹配、构建安装的不是当前调试版本。
  • 通知权限关闭不应直接阻止短信广播,但可能影响前台服务可见性和用户判断。

诊断顺序建议:

  1. 看 UI 权限状态和最近 boot/service 心跳。
  2. 发送短信时抓 logcat SmsReceive
  3. 如果 receiver 无日志,点“读取最新短信”确认短信是否入库。
  4. 如果短信已入库但 receiver 无日志重点排查权限、force-stop、HyperOS 自启动和省电。
  5. 如果 receiver 有日志但无验证码,排查 PDU/body/parser。

Data Model

新增 KeepAliveState 本地状态:

  • enabledByUser: 用户是否开启常驻保活。
  • serviceRunning: 最近一次服务 onCreate/onStartCommand/onDestroy 状态。
  • lastHeartbeatMillis: 服务最近心跳。
  • lastBootEvent: 最近一次 boot/package replaced 事件。
  • lastServiceStartFailure: 最近一次服务启动失败原因。
  • batteryOptimizationIgnored: 标准 Android 电池优化白名单状态。
  • manualAutostartConfirmed: 用户手动确认已开启小米自启动。
  • manualBatteryUnrestrictedConfirmed: 用户手动确认已设置省电无限制。
  • notificationEnabledHint: 通知是否可能可见。

保存方式优先使用 SharedPreferences,与现有 SmsCaptureStore 保持简单一致。

UI Strategy

首版继续使用现有 Java View UI不引入新框架

  • 增加“后台保活状态”区域:
    • 保活开关。
    • 服务运行状态。
    • 最近心跳。
    • 最近开机事件。
    • 最近启动失败原因。
  • 增加“系统设置”区域:
    • 打开应用详情。
    • 打开电池优化设置。
    • 请求忽略电池优化。
    • 打开小米自启动设置。
    • 人工确认自启动已开启。
    • 人工确认省电无限制已开启。
  • 增加“短信广播诊断”区域:
    • 最近 system_sms_broadcast 时间。
    • 最近 sms_inbox_* 时间。
    • 当收件箱有新短信但广播未到时提示“疑似广播未投递”。

Test Strategy

单元/本地测试:

  • KeepAliveStateStore 读写测试。
  • BootReceiver 在不同 action 下生成正确状态。
  • SmsKeepAliveService 的状态更新逻辑抽出为纯 Java 方法测试。
  • 设置 intent builder 测试:小米组件不可用时能 fallback。

ADB/真机验证:

  • adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -n com.smsreceive.app/.BootReceiver
  • 重启手机后验证保活服务是否恢复。
  • 开启保活通知后,后台 30 分钟、锁屏 30 分钟分别发送短信。
  • 开启/关闭小米自启动、开启/关闭省电无限制,对比短信广播和收件箱兜底结果。
  • 手动 force-stop 后发送短信,确认不承诺接收,并在再次打开 app 后展示诊断。
  • Android Doze 测试可参考官方命令 dumpsys deviceidle force-idleam set-inactive

Rollout Plan

  1. 完成本 OpenSpec 并 validate。
  2. 先实现 Java 层 BootReceiver、Foreground Service、通知和状态 UI。
  3. 再补小米设置入口和人工确认状态。
  4. 最后根据真机结果决定是否加入 native heartbeat 实验。
  5. 如果前台服务/开机恢复在 HyperOS 上仍不稳定,交付时明确记录限制和必须手动设置项。

Open Questions

  • 当前安装方式是 Android Studio/adb 直装,还是包管理器/文件管理器安装 APK这会影响 hard restricted SMS 权限表现。
  • 用户是否接受常驻通知一直显示。
  • 是否希望 app 开机后自动开启保活,还是必须用户先在 UI 中开启一次后才持久化。
  • 是否需要 native heartbeat 实验;建议第一轮 Java 方案真机验证后再决定。