fix(notify): eliminate duplicate alarm scheduling and fix test harness counting
Centralize all notification alarm scheduling through NotifyReceiver.scheduleExactNotification() with idempotence checks to prevent duplicate alarms. Implement one-alarm policy using setAlarmClock() only. Fix test harness alarm counting to deduplicate by Alarm handle. Plugin Changes: - Add ScheduleSource enum to track scheduling paths (INITIAL_SETUP, ROLLOVER_ON_FIRE, etc.) - Add DB-level idempotence check before scheduling (prevents logical duplicates) - Add explicit alarm cancellation before scheduling (safety net) - Implement one-alarm policy: use setAlarmClock() only, no setExact* fallbacks for same event - Add deep logging for all AlarmManager calls (variant, requestCode, pendingIntentHash) - Update all rollover paths (DailyNotificationReceiver, DailyNotificationWorker) to use centralized function with ROLLOVER_ON_FIRE source - Add @JvmStatic annotation to scheduleExactNotification for Java interop Test Harness Changes: - Fix get_plugin_alarm_count() to deduplicate by Alarm handle (prevents double-counting same alarm in main list and "Next wake from idle" section) - Update TEST 0 messaging: treat 0 alarms as race condition (inconclusive, not failure) - Make post-rollover check the authoritative assertion point (only fails on >1 or 0 alarms) - Remove redundant "Found 0 alarms - test may not be accurate" messages This fixes the duplicate alarm bug where two distinct AlarmManager entries were created for the same daily notification, violating the "one notification per day" contract.
This commit is contained in:
@@ -634,7 +634,7 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
// Cancel alarm using the scheduled time (used for request code)
|
||||
val nextRunAt = schedule.nextRunAt
|
||||
if (nextRunAt != null && nextRunAt > 0) {
|
||||
NotifyReceiver.cancelNotification(context, nextRunAt)
|
||||
NotifyReceiver.cancelNotification(context, scheduleId = schedule.id, triggerAtMillis = nextRunAt)
|
||||
cancelledAlarms++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -810,11 +810,19 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
|
||||
val nextRunTime = calculateNextRunTime(cronExpression)
|
||||
|
||||
// Generate scheduleId before scheduling (needed for stable requestCode)
|
||||
val scheduleId = options.getString("id") ?: "daily_reminder_${System.currentTimeMillis()}"
|
||||
|
||||
// Schedule AlarmManager notification
|
||||
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config)
|
||||
NotifyReceiver.scheduleExactNotification(
|
||||
context,
|
||||
nextRunTime,
|
||||
config,
|
||||
scheduleId = scheduleId,
|
||||
source = ScheduleSource.INITIAL_SETUP
|
||||
)
|
||||
|
||||
// Store schedule in database
|
||||
val scheduleId = options.getString("id") ?: "daily_reminder_${System.currentTimeMillis()}"
|
||||
val schedule = Schedule(
|
||||
id = scheduleId,
|
||||
kind = "notify",
|
||||
@@ -1392,7 +1400,9 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
nextRunTime,
|
||||
config,
|
||||
isStaticReminder = true,
|
||||
reminderId = scheduleId
|
||||
reminderId = scheduleId,
|
||||
scheduleId = scheduleId,
|
||||
source = ScheduleSource.TEST_NOTIFICATION
|
||||
)
|
||||
|
||||
// Always schedule prefetch 2 minutes before notification
|
||||
@@ -1470,7 +1480,7 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
val triggerAtMillis = options.getLong("triggerAtMillis") ?: return call.reject("triggerAtMillis is required")
|
||||
|
||||
val context = context ?: return call.reject("Context not available")
|
||||
val isScheduled = NotifyReceiver.isAlarmScheduled(context, triggerAtMillis)
|
||||
val isScheduled = NotifyReceiver.isAlarmScheduled(context, triggerAtMillis = triggerAtMillis)
|
||||
|
||||
val result = JSObject().apply {
|
||||
put("scheduled", isScheduled)
|
||||
@@ -1598,12 +1608,21 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
try {
|
||||
val nextRunTime = calculateNextRunTime(config.schedule)
|
||||
|
||||
// Generate scheduleId before scheduling (needed for stable requestCode)
|
||||
val scheduleId = "notify_${System.currentTimeMillis()}"
|
||||
|
||||
// Schedule AlarmManager notification
|
||||
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config)
|
||||
NotifyReceiver.scheduleExactNotification(
|
||||
context,
|
||||
nextRunTime,
|
||||
config,
|
||||
scheduleId = scheduleId,
|
||||
source = ScheduleSource.INITIAL_SETUP
|
||||
)
|
||||
|
||||
// Store schedule in database
|
||||
val schedule = Schedule(
|
||||
id = "notify_${System.currentTimeMillis()}",
|
||||
id = scheduleId,
|
||||
kind = "notify",
|
||||
cron = config.schedule,
|
||||
enabled = config.enabled,
|
||||
@@ -1687,7 +1706,14 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
FetchWorker.scheduleFetch(context, contentFetchConfig)
|
||||
|
||||
val nextRunTime = calculateNextRunTime(userNotificationConfig.schedule)
|
||||
NotifyReceiver.scheduleExactNotification(context, nextRunTime, userNotificationConfig)
|
||||
val scheduleId = "notify_${System.currentTimeMillis()}"
|
||||
NotifyReceiver.scheduleExactNotification(
|
||||
context,
|
||||
nextRunTime,
|
||||
userNotificationConfig,
|
||||
scheduleId = scheduleId,
|
||||
source = ScheduleSource.INITIAL_SETUP
|
||||
)
|
||||
|
||||
// Store both schedules
|
||||
val fetchSchedule = Schedule(
|
||||
@@ -1896,7 +1922,7 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
|
||||
// Only check AlarmManager status for "notify" schedules with nextRunAt
|
||||
if (schedule.kind == "notify" && schedule.nextRunAt != null) {
|
||||
val isScheduled = NotifyReceiver.isAlarmScheduled(context, schedule.nextRunAt!!)
|
||||
val isScheduled = NotifyReceiver.isAlarmScheduled(context, scheduleId = schedule.id, triggerAtMillis = schedule.nextRunAt!!)
|
||||
scheduleJson.put("isActuallyScheduled", isScheduled)
|
||||
} else {
|
||||
scheduleJson.put("isActuallyScheduled", false)
|
||||
|
||||
Reference in New Issue
Block a user