BackgroundAlertManager.swift 5.8 KB

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