You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

AlarmManager异常:设置多餐饮时段时重复推送所有通知求助

问题分析与解决方案

核心问题定位

你的问题根源在于Flow的collect持续监听导致重复设置闹钟,以及未正确处理已过时间的闹钟逻辑,具体如下:

  1. 调用scheduleMealAlarm时使用collect持续监听meal数据变化,只要Repository中任何meal数据更新(比如设置午餐时),所有已启动的collect协程都会重新执行设置闹钟的逻辑,导致旧闹钟被重复设置,甚至触发已过时间的闹钟。
  2. 未判断设置时间是否早于当前时间,若当前时间已过设置点,setRepeating会立即触发一次闹钟,后续叠加到正常触发时间点导致批量通知。

修复方案

1. 替换collect为单次数据获取

设置闹钟只需要当前的meal配置,不需要持续监听变化,用first()替代collect,获取一次数据后结束协程,避免重复设置。

2. 处理已过时间的闹钟

判断设置时间是否早于当前时间,若是则将日期加一天,避免立即触发。

3. 确保PendingIntent匹配一致性

在cancelMealAlarm中保持Intent与scheduleMealAlarm一致(添加mealName extra),确保取消闹钟时能精准匹配。

修复后的代码

CCAlarmManager 修复版
class CCAlarmManager @Inject constructor(
    private val alarmManager: AlarmManager,
    private val mealTimeScreenRepositoryImpl: MealTimeScreenRepositoryImpl,
    private val context: Context
) {
    private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    fun scheduleMealAlarm(name: String) {
        coroutineScope.launch {
            // 仅获取当前最新的meal数据,而非持续监听
            val meal = mealTimeScreenRepositoryImpl.getMealTimeByName(name).firstOrNull() ?: return@launch
            
            val intent = Intent(context, CCAlarmReceiver::class.java).apply {
                putExtra("mealName", meal.name)
            }
            val pendingIntent = PendingIntent.getBroadcast(
                context,
                meal.id, // 确保meal.id是唯一值(如1=早餐,2=午餐等)
                intent,
                PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
            )

            // 先取消旧闹钟
            alarmManager.cancel(pendingIntent)

            val hours = meal.time.split(":")[0].take(2).toInt()
            val minutes = meal.time.split(":")[1].take(2).toInt()
            val calendar = Calendar.getInstance().apply {
                set(Calendar.HOUR_OF_DAY, hours)
                set(Calendar.MINUTE, minutes)
                set(Calendar.SECOND, 0)
                set(Calendar.MILLISECOND, 0)
                // 若设置时间早于当前时间,日期加一天
                if (timeInMillis < System.currentTimeMillis()) {
                    add(Calendar.DAY_OF_YEAR, 1)
                }
            }

            // 建议使用setExactAndAllowWhileIdle替代setRepeating,适配Android 6+的省电策略
            alarmManager.setExactAndAllowWhileIdle(
                AlarmManager.RTC_WAKEUP,
                calendar.timeInMillis,
                pendingIntent
            )
            // 若需要重复闹钟,可在BroadcastReceiver中重新设置下一次闹钟(因为setRepeating在Android 6+后精度无法保证)
        }
    }

    fun cancelMealAlarm(name: String) {
        coroutineScope.launch {
            val meal = mealTimeScreenRepositoryImpl.getMealTimeByName(name).firstOrNull() ?: return@launch
            
            val intent = Intent(context, CCAlarmReceiver::class.java).apply {
                putExtra("mealName", meal.name) // 保持与设置时的Intent一致
            }
            val pendingIntent = PendingIntent.getBroadcast(
                context,
                meal.id,
                intent,
                PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_NO_CREATE // 使用FLAG_NO_CREATE避免创建新的PendingIntent
            )
            pendingIntent?.let { alarmManager.cancel(it) }
        }
    }
}
CCAlarmReceiver 优化版(支持重复闹钟)

如果需要重复每天的闹钟,建议在onReceive中重新设置下一天的闹钟,因为setRepeating在Android 6及以上版本的省电模式下精度会下降:

class CCAlarmReceiver: BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent?) {
        val mealName = intent?.getStringExtra("mealName") ?: return

        // 发送通知
        val notification = NotificationCompat.Builder(context, "cc_notifications")
            .setSmallIcon(R.drawable.logo)
            .setContentTitle("Eating time")
            .setContentText("Time to eat $mealName")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .build()

        val notificationManager = context.getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager
        // 使用唯一的通知ID,建议直接用meal的id而非hashCode,避免冲突
        val notificationId = when(mealName) {
            "breakfast" -> 1
            "lunch" -> 2
            "dinner" -> 3
            "snack" -> 4
            else -> mealName.hashCode()
        }
        notificationManager.notify(notificationId, notification)

        // 重新设置下一天的闹钟(实现重复提醒)
        val ccAlarmManager = // 这里需要获取CCAlarmManager实例,可通过DI或其他方式
        ccAlarmManager.scheduleMealAlarm(mealName)
    }
}

额外注意事项

  • 确保meal.id是每个时段的唯一整数(比如早餐=1,午餐=2等),避免PendingIntent被覆盖。
  • Android 12及以上版本需要在Manifest中申请POST_NOTIFICATIONS权限,否则通知无法弹出。
  • 对于重复闹钟,setExactAndAllowWhileIdle比setRepeating更可靠,因为后者在省电模式下会被系统延迟或忽略。

内容的提问来源于stack exchange,提问作者Егор Сауткин

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 05:09:49