BackgroundAlertManager.swift 5.8 KB

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