BackgroundAlertManager.swift 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. // LoopFollow
  2. // BackgroundAlertManager.swift
  3. import Foundation
  4. import UserNotifications
  5. /// Enum representing different background alert durations.
  6. enum BackgroundAlertDuration: TimeInterval, CaseIterable {
  7. case sixMinutes = 360 // 6 minutes in seconds
  8. case twelveMinutes = 720 // 12 minutes in seconds
  9. case eighteenMinutes = 1080 // 18 minutes in seconds
  10. }
  11. /// Enum representing unique identifiers for each background alert.
  12. enum BackgroundAlertIdentifier: String, CaseIterable {
  13. case sixMin = "loopfollow.background.alert.6min"
  14. case twelveMin = "loopfollow.background.alert.12min"
  15. case eighteenMin = "loopfollow.background.alert.18min"
  16. }
  17. class BackgroundAlertManager {
  18. static let shared = BackgroundAlertManager()
  19. private init() {}
  20. /// Flag indicating whether background alerts are currently scheduled.
  21. private var isAlertScheduled: Bool = false
  22. /// Title prefix for all background refresh notifications.
  23. private let notificationTitlePrefix = "LoopFollow Background Refresh"
  24. /// Timestamp of the last scheduled background alert.
  25. private var lastScheduleDate: Date?
  26. /// Start scheduling background alerts.
  27. func startBackgroundAlert() {
  28. isAlertScheduled = true
  29. // Force execution to bypass throttle when starting
  30. scheduleBackgroundAlert(force: true)
  31. }
  32. /// Stop all scheduled background alerts.
  33. func stopBackgroundAlert() {
  34. isAlertScheduled = false
  35. removeDeliveredNotifications()
  36. cancelBackgroundAlerts()
  37. }
  38. /// (Re)schedule all background alerts based on predefined durations.
  39. /// - Parameter force: When true, the scheduling is executed regardless of throttle constraints.
  40. func scheduleBackgroundAlert(force: Bool = false) {
  41. guard isAlertScheduled, Storage.shared.backgroundRefreshType.value != .none else { return }
  42. // Throttle execution if not forced: only run once every 10 seconds.
  43. if !force {
  44. let now = Date()
  45. if let lastDate = lastScheduleDate, now.timeIntervalSince(lastDate) < 10 {
  46. return
  47. }
  48. lastScheduleDate = now
  49. }
  50. removeDeliveredNotifications()
  51. let isBluetoothActive = Storage.shared.backgroundRefreshType.value.isBluetooth
  52. let expectedHeartbeat = BLEManager.shared.expectedHeartbeatInterval()
  53. // Define alerts
  54. let alerts: [BackgroundAlert] = [
  55. BackgroundAlert(
  56. identifier: BackgroundAlertIdentifier.sixMin.rawValue,
  57. timeInterval: BackgroundAlertDuration.sixMinutes.rawValue,
  58. body: isBluetoothActive
  59. ? "App inactive for 6 minutes. Verify Bluetooth connectivity."
  60. : "App inactive for 6 minutes. Open to resume."
  61. ),
  62. BackgroundAlert(
  63. identifier: BackgroundAlertIdentifier.twelveMin.rawValue,
  64. timeInterval: BackgroundAlertDuration.twelveMinutes.rawValue,
  65. body: isBluetoothActive
  66. ? "App inactive for 12 minutes. Verify Bluetooth connectivity."
  67. : "App inactive for 12 minutes. Open to resume."
  68. ),
  69. BackgroundAlert(
  70. identifier: BackgroundAlertIdentifier.eighteenMin.rawValue,
  71. timeInterval: BackgroundAlertDuration.eighteenMinutes.rawValue,
  72. body: isBluetoothActive
  73. ? "App inactive for 18 minutes. Verify Bluetooth connectivity."
  74. : "App inactive for 18 minutes. Open to resume."
  75. ),
  76. ]
  77. for alert in alerts {
  78. // Skip if the expected heartbeat interval matches or exceeds 1.2x the alert time interval
  79. if let heartbeat = expectedHeartbeat, heartbeat * 1.2 >= alert.timeInterval {
  80. continue
  81. }
  82. let content = createNotificationContent(for: notificationTitlePrefix, body: alert.body)
  83. let trigger = UNTimeIntervalNotificationTrigger(timeInterval: alert.timeInterval, repeats: false)
  84. let request = UNNotificationRequest(identifier: alert.identifier, content: content, trigger: trigger)
  85. UNUserNotificationCenter.current().add(request) { error in
  86. if let error = error {
  87. LogManager.shared.log(category: .general, message: "Error scheduling \(alert.timeInterval / 60)-minute background alert: \(error)")
  88. }
  89. }
  90. }
  91. }
  92. /// Create notification content with a given title and body.
  93. /// - Parameters:
  94. /// - title: The title of the notification.
  95. /// - body: The body text of the notification.
  96. /// - Returns: Configured `UNMutableNotificationContent` object.
  97. private func createNotificationContent(for title: String, body: String) -> UNMutableNotificationContent {
  98. let content = UNMutableNotificationContent()
  99. content.title = title
  100. content.body = body
  101. content.sound = .defaultCritical
  102. content.categoryIdentifier = "loopfollow.background.alert"
  103. return content
  104. }
  105. /// Cancel all scheduled background alerts.
  106. private func cancelBackgroundAlerts() {
  107. let identifiers = BackgroundAlertIdentifier.allCases.map { $0.rawValue }
  108. UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
  109. }
  110. /// Remove all delivered notifications.
  111. private func removeDeliveredNotifications() {
  112. let identifiers = BackgroundAlertIdentifier.allCases.map { $0.rawValue }
  113. UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
  114. }
  115. }
  116. /// Struct representing a single background alert.
  117. struct BackgroundAlert {
  118. let identifier: String
  119. let timeInterval: TimeInterval
  120. let body: String
  121. }