diff --git a/android/app/src/main/kotlin/es/freetimelab/pluriwave/AlarmScheduler.kt b/android/app/src/main/kotlin/es/freetimelab/pluriwave/AlarmScheduler.kt index e19210f..0b5d0f9 100644 --- a/android/app/src/main/kotlin/es/freetimelab/pluriwave/AlarmScheduler.kt +++ b/android/app/src/main/kotlin/es/freetimelab/pluriwave/AlarmScheduler.kt @@ -75,14 +75,49 @@ class AlarmScheduler(private val context: Context) { fadeInSegundos = fadeInSegundos.coerceIn(0, 60), timezoneId = TimeZone.getDefault().id ) - return scheduleSpec(spec, persistOnSuccess = true) - } - - private fun scheduleSpec(spec: NativeAlarmSpec, persistOnSuccess: Boolean): Boolean { - val nextTrigger = computeNextTriggerMillis(spec) Log.d( tag, - "alarm.schedule id=${spec.id} title=${spec.title} trigger=$nextTrigger type=${spec.scheduleType} snooze=${spec.snoozeUntilMillis} canExact=${canScheduleExactAlarms()}" + "alarm.channel spec id=$id weekdays=$weekdays type=${spec.scheduleType} " + + "dartTrigger=$triggerAtMillis lastHandled=$lastHandledAtMillis" + ) + // Dart is the single source of truth for WHEN the next occurrence is: + // the trigger it sends is exactly what the user sees in the UI, so a + // fresh channel call must arm THAT instant, never a native recompute + // that can disagree with it (two independent next-occurrence engines + // WILL diverge someday — observed on-device: Dart said "today", the + // native weekday scan armed next week, and the alarm silently never + // rang). The native computation remains ONLY for autonomous re-arms + // where no fresh Dart data exists (onAlarmFired's next occurrence, + // boot/persisted reschedules) via scheduleSpec's own recompute. + return scheduleSpec(spec, persistOnSuccess = true, trustDartTrigger = true) + } + + private fun scheduleSpec( + spec: NativeAlarmSpec, + persistOnSuccess: Boolean, + trustDartTrigger: Boolean = false + ): Boolean { + // A fresh channel call carries Dart's own next-occurrence verdict — + // the one the UI shows. Honor it whenever it is still in the future + // (or just-passed within the shared 90s imminence window, matching + // Dart's toleranciaDisparoInminente: AlarmManager fires a past + // trigger immediately, which is exactly the contract). Only when the + // Dart trigger is genuinely stale (e.g. a resync replaying an old + // stored value long past) does the native recompute take over as the + // fallback engine. + val now = System.currentTimeMillis() + val nextTrigger = + if (trustDartTrigger && + spec.snoozeUntilMillis == null && + spec.triggerAtMillis > now - IMMINENT_TOLERANCE_MILLIS + ) { + spec.triggerAtMillis + } else { + computeNextTriggerMillis(spec) + } + Log.d( + tag, + "alarm.schedule id=${spec.id} title=${spec.title} trigger=$nextTrigger type=${spec.scheduleType} snooze=${spec.snoozeUntilMillis} trustDart=$trustDartTrigger canExact=${canScheduleExactAlarms()}" ) if (nextTrigger == null) { Log.d(tag, "alarm.schedule no next trigger id=${spec.id}")