Skip to content

Proposal:无需后台常驻的醒目全屏语音提醒 #114

Description

@gac0812

提案:无需后台常驻的醒目全屏语音提醒

1. 功能描述

本模块在 App 创建提醒后,将未来触发时间登记到 Android 系统 AlarmManager。到点时,即使 App 不在前台或进程已被普通回收,系统仍可重新启动 App 的广播接收器;App 随后启动前台服务,在当前桌面或其他普通 App 上方显示可操作的全屏提醒,并使用闹钟音频通道播放语音。

用户可以在 App 中静默取消尚未触发的提醒,触发后通过全屏界面的“停止闹钟”结束提醒。当前测试实现使用 Edge TTS 生成的内置 MP3;提醒方式可在后续扩展为语音、震动、语音加震动或仅界面。

核心能力包括:

维度 能力 示例
系统级调度 使用 AlarmManager.setAlarmClock() 登记未来任务 创建明天 09:00 的提醒后,系统负责到点唤醒广播接收器
进程独立触发 Receiver 由清单声明,App 普通进程不存在时可被系统重新创建 用户划走 App 或系统普通回收进程后,仍能触发
跨 App 可见 前台服务添加 TYPE_APPLICATION_OVERLAY 全屏窗口 用户正在聊天、浏览网页或停留在桌面时,仍看到停止界面
锁屏后备 高优先级闹钟通知关联 fullScreenIntent 和锁屏 Activity 息屏/锁屏时显示 RingActivity 后备页面
语音提醒 App 自己的 MediaPlayer 播放内置 Edge TTS MP3,并循环播放 播报“您设置的时间到了,请点击停止闹钟”
停止处理 App 内取消未来任务,或在全屏界面点击停止 取消任务后不跳转系统时钟页面

这里的“系统闹钟”指 Android AlarmManager 的系统级闹钟任务。系统可以将下一个闹钟显示在状态栏或锁屏,但不承诺一定把它写入小米等厂商“时钟”App 的闹钟列表;本模块的核心目标是系统可靠触发和用户可见处理界面。

2. 用户故事

目标用户:希望提醒足够醒目,并且不希望为了收到提醒而让 App 一直保持后台常驻的用户。

用户在 App 中创建一个未来提醒后,希望可以正常退出或关闭 App,无需手动维持后台运行。到点时,即使用户正在其他 App、停留在桌面、处于锁屏状态,或 App 进程已被系统普通回收,手机仍应出现醒目的全屏提醒并播报语音,让用户立即知道提醒来自哪里以及应该如何处理。

用户点击“停止闹钟”后,希望语音、悬浮窗、通知和前台服务全部结束,不需要进入系统时钟页面手动处理。

用户在 App 内取消一个尚未触发的提醒,希望手机端立即静默取消对应的系统任务,不需要跳转到系统时钟页面。

用户手机暂时离线时,希望已经登记的本地提醒仍能触发,不依赖网络连接。

3. 现有做法及不足

常见的简单实现通常存在以下问题:

  • 只在 Activity 中使用 Handler 或计时器,App 退出后任务消失。
  • 只发送普通通知,用户在其他 App 中可能只看到通知或听到声音,不知道如何停止。
  • 只使用 fullScreenIntent,但在解锁状态下 Android/HyperOS 可能只显示抬头通知,不保证 Activity 抢占前台。
  • 触发时临时调用在线 TTS,断网或厂商限制 TTS 进程时可能无声。
  • 删除一个未来任务时无条件停止全局响铃服务,可能影响另一个正在响铃的提醒。

本提案采用 AlarmManager → BroadcastReceiver → Foreground Service → Overlay/Notification/MediaPlayer 链路解决上述问题。时间选择器只是产生 triggerAtMillis 的一种 UI,不是本能力的核心。

4. 本期范围

  1. 接收一个未来的绝对时间戳 triggerAtMillis,登记 Android AlarmManager.setAlarmClock() 任务。
  2. 持久化触发时间和取消/重建系统任务所需的稳定字段。
  3. 使用清单声明的显式 BroadcastReceiver,保证 App 普通进程死亡后仍可由系统重新启动。
  4. 广播到点后启动媒体播放类型的前台服务,并立即调用 startForeground()
  5. 服务发布高重要级别、CATEGORY_ALARM 的持续通知。
  6. 获得悬浮窗授权后,服务使用 TYPE_APPLICATION_OVERLAY 展示包含当前时间、提醒来源和“停止闹钟”按钮的全屏界面。
  7. 锁屏或系统允许时,通过通知 fullScreenIntent 提供 RingActivity 后备页面。
  8. 使用 APK 内置 Edge TTS MP3,通过 USAGE_ALARM + CONTENT_TYPE_SPEECH 播放并循环。
  9. App 内取消尚未触发的提醒时,静默取消对应的系统任务。
  10. 停止当前响铃时,移除悬浮窗、停止播放器、取消通知、停止服务并关闭响铃 Activity。
  11. 引导用户授权精确闹钟、通知、全屏通知、悬浮窗和电池优化相关权限。

5. 明确不做

  • 不依赖 Activity 常驻、Handler 计时器或 App 一直处于前台。
  • 不承诺用户强行停止 App 后仍能绕过 Android stopped 状态触发。
  • 不承诺支付页、密码页、授权页等设置了 HIDE_NON_SYSTEM_OVERLAY_WINDOWS 的敏感页面一定能显示第三方悬浮窗。
  • 不承诺解锁状态下全屏通知一定抢占当前 Activity;解锁场景主要依赖悬浮窗。
  • 不在响铃触发时依赖网络在线合成 TTS;当前语音资源在构建阶段生成并随 APK 打包。
  • 不承诺卸载后保留系统任务。
  • 当前版本不承诺手机重启后自动恢复任务;若产品需要,另行增加 BOOT_COMPLETED 重建方案。
  • 当前版本不实现多个提醒同一时刻触发时的完整队列体验;生产版需要增加活动响铃队列。
  • 不把本模块描述为已经写入小米“时钟”App 的用户闹钟列表;实际使用的是 Android 系统 AlarmManager
  • 不在本期扩展地点、天气、蓝牙、设备状态等智能情境提醒。

6. 关键决策

决策点 备选方案 选择 理由
系统调度 Activity 计时器 / 普通通知定时 / AlarmManager setAlarmClock() 任务由系统保存,适合用户可感知的精确闹钟
到点入口 动态 Receiver / 清单 Receiver 清单显式 Receiver App 进程不存在时,系统仍能重新创建组件
后台执行 普通 Service / Foreground Service 媒体播放 Foreground Service Android 8+ 后台启动和持续播放需要前台服务资格
解锁时可见 只用 full-screen Activity / Overlay Overlay + full-screen 通知后备 解锁时系统可能不允许通知 Activity 抢前台,Overlay 更稳定
锁屏处理 只用 Overlay / 只用 Activity 两者结合 Overlay 负责普通界面,锁屏 Activity 负责系统后备路径
取消方式 跳转系统时钟 / App 本地静默取消 App 本地静默取消 用户不需要离开当前 App,系统任务由 App 自己结束
语音来源 触发时在线 TTS / 系统 TTS / 内置 MP3 内置 MP3 + MediaPlayer 不依赖网络和独立 TTS 进程,音频行为可控
重启恢复 不恢复 / 开机 Receiver 重建 本期不做,后续可选 Android 默认不保证 AlarmManager 任务跨重启保留

7. 边界与异常

情况 处理方式
精确闹钟权限未开启 不调用 setAlarmClock();引导用户进入 ACTION_REQUEST_SCHEDULE_EXACT_ALARM
悬浮窗权限未开启 仍可保留通知/后备路径,但不能承诺解锁时全屏覆盖;引导 ACTION_MANAGE_OVERLAY_PERMISSION
通知权限或全屏通知权限未开启 保留可用路径并明确提示;通知权限缺失会影响前台服务可见性和通知入口
电池优化或 HyperOS 后台限制 引导用户设置“无限制”、允许后台弹出/自启动;不能用 App 代码绕过厂商策略
选择的时间已过去 拒绝创建并要求重新提供未来时间戳
系统任务取消信息缺失 记录取消失败原因,不删除本地记录,避免 UI 与系统状态不一致
用户取消未来任务 只取消对应的系统任务,不停止其他正在响铃的服务
当前提醒正在响铃 停止播放器、Overlay、通知和响铃 Activity,并清除当前任务记录
App 被强行停止 不承诺到点触发,等待用户重新启动 App 后恢复可用
手机重启 当前版本不自动恢复;生产版增加 RECEIVE_BOOT_COMPLETED 和未来任务重建
敏感系统页面隐藏 Overlay 接受 Android 安全行为,保留通知/音频可用性,不声称能覆盖该页面
MP3 播放失败 记录错误并保留全屏 UI/通知,生产版可增加本地备用音频
用户关闭闹钟通知渠道声音 当前 App 语音由 MediaPlayer 播放,通知渠道不作为唯一声音来源;仍需尊重系统勿扰和音量设置
同一时间多个提醒触发 当前测试版只保证基础提醒能力;生产版增加活动响铃队列或合并提醒界面

8. 基本概念与信息结构

8.1 LocalReminderRecord(设备本地)

LocalReminderRecord
├─ triggerAtMillis: long(未来绝对时间)
├─ systemTaskBinding: 重建和取消系统任务所需的内部字段
├─ reminderMode: VOICE | VIBRATION | VOICE_AND_VIBRATION(可扩展字段)
├─ status: scheduled | firing | cancelled | completed
└─ createdAt / updatedAt

系统任务绑定字段只用于 App 自己重建和取消 PendingIntent,不作为用户界面或业务层概念。

当前测试 APK 已实现 VOICEVIBRATIONVOICE_AND_VIBRATION 可以复用同一服务,在触发时选择是否创建 MediaPlayerVibrator

8.2 触发链路

业务模块提供 triggerAtMillis
 → 构造系统广播 PendingIntent 并持久化取消所需字段
 → AlarmManager.setAlarmClock()
 → Android 到点发送显式广播
 → AlarmReceiver.startForegroundService()
 → AlarmSoundService.startForeground()
 → Notification + TYPE_APPLICATION_OVERLAY + MediaPlayer
 → 用户点击“停止闹钟”
 → cancel overlay / player / notification / service

8.3 提醒方式扩展

提醒方式不应影响系统调度任务本身:

VOICE
 → MediaPlayer + USAGE_ALARM

VIBRATION
 → Vibrator/VibratorManager + VIBRATE 权限

VOICE_AND_VIBRATION
 → 同时启用 MediaPlayer 与 Vibrator

SCREEN_ONLY
 → 只显示 Overlay/Activity,不播放声音或震动

通知渠道的声音和震动配置创建后受系统管理,因此“仅震动”最好由服务直接控制 Vibrator,而不是依赖动态修改同一个通知渠道。

9. 验收标准

  1. 业务模块提供一个未来 triggerAtMillis 后,设备成功登记 Android AlarmManager 闹钟;不要求时间必须来自某一种选择器。
  2. 用户切换到其他 App、回到桌面或 App 进程被普通回收后,任务仍能到点触发。
  3. 到点后确认前台服务正在运行、通知存在、普通其他 App 上方出现全屏 Overlay,且界面明确显示提醒来源和停止按钮。
  4. 播放 Edge TTS 语音时使用 USAGE_ALARM;断网情况下仍能播放 APK 内置 MP3。
  5. 在 App 内取消尚未触发的提醒后,系统任务被静默取消,不跳转系统时钟页面。
  6. 点击停止后,Overlay 消失、MediaPlayer 停止、通知消失、服务结束,且不会留下循环回调。
  7. 锁屏/息屏时测试 fullScreenIntentRingActivity 后备路径;接受系统对全屏通知的最终调度权。
  8. 在支付、授权或密码等敏感页面测试时,接受第三方 Overlay 被系统隐藏的限制。
  9. 小米 14 / HyperOS 真机检查悬浮窗、通知、全屏通知、电池优化和后台弹出权限。
  10. 文档明确:强行停止、卸载、重启恢复和多个同时触发队列是否属于当前版本能力。

ADB 验收证据

查看系统闹钟:

adb shell dumpsys alarm | rg "包名|FIRE_ALARM"

确认系统已保存应用闹钟任务:

adb shell dumpsys activity intents | rg "包名|broadcastIntent"

确认悬浮窗:

adb shell dumpsys window windows | rg "APPLICATION_OVERLAY|包名"

确认前台服务:

adb shell dumpsys activity services 包名 | rg "AlarmSoundService|isForeground"

确认闹钟语音播放器:

adb shell dumpsys audio | rg "包名|USAGE_ALARM|CONTENT_TYPE_SPEECH"

测试普通进程死亡可使用 adb shell am kill 包名;不要把 adb shell am force-stop 包名 当成普通进程死亡场景。

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions