AlarmManager.swift 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  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. //TODO: Handle default snooze for notofication snoze
  70. //TODO: Check interval type handling
  71. func performSnooze(_ minutes: Int? = nil) {
  72. if let alarmID = Observable.shared.currentAlarm.value {
  73. var alarms = Storage.shared.alarms.value
  74. if let idx = alarms.firstIndex(where: { $0.id == alarmID }) {
  75. alarms[idx].snoozedUntil = Date().addingTimeInterval(
  76. TimeInterval((minutes ?? 5) * 60)) // fix default value
  77. }
  78. Storage.shared.alarms.value = alarms
  79. AlarmSound.stop()
  80. Observable.shared.currentAlarm.value = nil
  81. }
  82. }
  83. }