AlarmManager.swift 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. //
  2. // AlarmManager.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2025-03-15.
  6. // Copyright © 2025 Jon Fawcett. All rights reserved.
  7. //
  8. import Foundation
  9. class AlarmManager {
  10. static let shared = AlarmManager()
  11. private let evaluators: [AlarmType: AlarmCondition]
  12. private init(
  13. conditionTypes: [AlarmCondition.Type] = [
  14. BuildExpireCondition.self
  15. // TODO: add other condition types here
  16. ]
  17. ) {
  18. var dict = [AlarmType: AlarmCondition]()
  19. conditionTypes.forEach { dict[$0.type] = $0.init() }
  20. evaluators = dict
  21. }
  22. //TODO: Somehow we need to silent the current alarm if the current one is no longer active.
  23. func checkAlarms(data: AlarmData) {
  24. let now = Date()
  25. let alarms = Storage.shared.alarms.value
  26. let sorted = alarms.sorted { lhs, rhs in
  27. // Primary: type priority
  28. if lhs.type.priority != rhs.type.priority {
  29. return lhs.type.priority < rhs.type.priority
  30. }
  31. // Secondary: threshold ordering if applicable
  32. if let asc = lhs.type.thresholdSortAscending {
  33. let leftVal = lhs.threshold ?? (asc ? Float.infinity : -Float.infinity)
  34. let rightVal = rhs.threshold ?? (asc ? Float.infinity : -Float.infinity)
  35. return asc ? leftVal < rightVal : leftVal > rightVal
  36. }
  37. // Tertiary: fallback to insertion order
  38. return false
  39. }
  40. var skipType: AlarmType? = nil
  41. for alarm in sorted {
  42. // If there is already an active (snoozed) alarm of this type, skip to next [type]
  43. if alarm.type == skipType {
  44. continue
  45. }
  46. // If the alarm itself is snoozed, skip lower‑priority alarms of the same type.
  47. if let until = alarm.snoozedUntil, until > now {
  48. skipType = alarm.type
  49. continue
  50. }
  51. // Evaluate the alarm condition.
  52. guard let checker = evaluators[alarm.type],
  53. checker
  54. .shouldFire(
  55. alarm: alarm,
  56. data: data,
  57. now: now,
  58. config: Storage.shared.alarmConfiguration.value
  59. )
  60. else {
  61. continue
  62. }
  63. // Fire the alarm and break the loop; we only allow one alarm per evaluation tick.
  64. Observable.shared.currentAlarm.value = alarm.id
  65. alarm.trigger(config: Storage.shared.alarmConfiguration.value, now: now)
  66. break
  67. }
  68. }
  69. func performSnooze(_ snoozeUnits: Int? = nil) {
  70. guard let alarmID = Observable.shared.currentAlarm.value else { return }
  71. var alarms = Storage.shared.alarms.value
  72. if let idx = alarms.firstIndex(where: { $0.id == alarmID }) {
  73. let alarm = alarms[idx]
  74. let units = snoozeUnits ?? alarm.snoozeDuration
  75. let snoozeSeconds = Double(units) * alarm.type.timeUnit.seconds
  76. alarms[idx].snoozedUntil = Date().addingTimeInterval(snoozeSeconds)
  77. Storage.shared.alarms.value = alarms
  78. AlarmSound.stop()
  79. Observable.shared.currentAlarm.value = nil
  80. }
  81. }
  82. }