BackgroundAlertManager.swift 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  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. /// Start scheduling background alerts.
  30. func startBackgroundAlert() {
  31. isAlertScheduled = true
  32. scheduleBackgroundAlert()
  33. }
  34. /// Stop all scheduled background alerts.
  35. func stopBackgroundAlert() {
  36. isAlertScheduled = false
  37. removeDeliveredNotifications()
  38. cancelBackgroundAlerts()
  39. }
  40. /// (Re)schedule all background alerts based on predefined durations.
  41. func scheduleBackgroundAlert() {
  42. removeDeliveredNotifications()
  43. guard isAlertScheduled, Storage.shared.backgroundRefreshType.value != .none else { return }
  44. let isBluetoothActive = Storage.shared.backgroundRefreshType.value.isBluetooth
  45. let expectedHeartbeat = BLEManager.shared.expectedHeartbeatInterval()
  46. // Define alerts
  47. let alerts: [BackgroundAlert] = [
  48. BackgroundAlert(
  49. identifier: BackgroundAlertIdentifier.sixMin.rawValue,
  50. timeInterval: BackgroundAlertDuration.sixMinutes.rawValue,
  51. body: isBluetoothActive
  52. ? "App inactive for 6 minutes. Verify Bluetooth connectivity."
  53. : "App inactive for 6 minutes. Open to resume."
  54. ),
  55. BackgroundAlert(
  56. identifier: BackgroundAlertIdentifier.twelveMin.rawValue,
  57. timeInterval: BackgroundAlertDuration.twelveMinutes.rawValue,
  58. body: isBluetoothActive
  59. ? "App inactive for 12 minutes. Verify Bluetooth connectivity."
  60. : "App inactive for 12 minutes. Open to resume."
  61. ),
  62. BackgroundAlert(
  63. identifier: BackgroundAlertIdentifier.eighteenMin.rawValue,
  64. timeInterval: BackgroundAlertDuration.eighteenMinutes.rawValue,
  65. body: isBluetoothActive
  66. ? "App inactive for 18 minutes. Verify Bluetooth connectivity."
  67. : "App inactive for 18 minutes. Open to resume."
  68. )
  69. ]
  70. for alert in alerts {
  71. // Skip if the expected heartbeat interval matches or exceeds 1.2x the alert time interval
  72. if let heartbeat = expectedHeartbeat, heartbeat >= alert.timeInterval * 1.2 {
  73. continue
  74. }
  75. let content = createNotificationContent(for: notificationTitlePrefix, body: alert.body)
  76. let trigger = UNTimeIntervalNotificationTrigger(timeInterval: alert.timeInterval, repeats: false)
  77. let request = UNNotificationRequest(identifier: alert.identifier, content: content, trigger: trigger)
  78. UNUserNotificationCenter.current().add(request) { error in
  79. if let error = error {
  80. LogManager.shared.log(category: .general, message: "Error scheduling \(alert.timeInterval / 60)-minute background alert: \(error)")
  81. }
  82. }
  83. }
  84. }
  85. /// Create notification content with a given title and body.
  86. /// - Parameters:
  87. /// - title: The title of the notification.
  88. /// - body: The body text of the notification.
  89. /// - Returns: Configured `UNMutableNotificationContent` object.
  90. private func createNotificationContent(for title: String, body: String) -> UNMutableNotificationContent {
  91. let content = UNMutableNotificationContent()
  92. content.title = title
  93. content.body = body
  94. content.sound = .defaultCritical
  95. content.categoryIdentifier = "loopfollow.background.alert"
  96. return content
  97. }
  98. /// Cancel all scheduled background alerts.
  99. private func cancelBackgroundAlerts() {
  100. let identifiers = BackgroundAlertIdentifier.allCases.map { $0.rawValue }
  101. UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
  102. }
  103. /// Remove all delivered notifications
  104. private func removeDeliveredNotifications() {
  105. let identifiers = BackgroundAlertIdentifier.allCases.map { $0.rawValue }
  106. UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
  107. }
  108. }
  109. /// Struct representing a single background alert.
  110. struct BackgroundAlert {
  111. let identifier: String
  112. let timeInterval: TimeInterval
  113. let body: String
  114. }