| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- // LoopFollow
- // BackgroundAlertManager.swift
- import Foundation
- import UserNotifications
- /// Enum representing different background alert durations.
- enum BackgroundAlertDuration: TimeInterval, CaseIterable {
- case sixMinutes = 360 // 6 minutes in seconds
- case twelveMinutes = 720 // 12 minutes in seconds
- case eighteenMinutes = 1080 // 18 minutes in seconds
- }
- /// Unique identifiers for each background alert, scoped to the current bundle
- /// so multiple LoopFollow instances don't interfere with each other's notifications.
- enum BackgroundAlertIdentifier: CaseIterable {
- case sixMin
- case twelveMin
- case eighteenMin
- private static let prefix = Bundle.main.bundleIdentifier ?? "loopfollow"
- var rawValue: String {
- switch self {
- case .sixMin: "\(Self.prefix).background.alert.6min"
- case .twelveMin: "\(Self.prefix).background.alert.12min"
- case .eighteenMin: "\(Self.prefix).background.alert.18min"
- }
- }
- static let categoryIdentifier = "\(prefix).background.alert"
- }
- class BackgroundAlertManager {
- static let shared = BackgroundAlertManager()
- private init() {}
- /// Flag indicating whether background alerts are currently scheduled.
- private var isAlertScheduled: Bool = false
- /// Title prefix for all background refresh notifications.
- private let notificationTitlePrefix = "LoopFollow Background Refresh"
- /// Timestamp of the last scheduled background alert.
- private var lastScheduleDate: Date?
- /// Start scheduling background alerts.
- func startBackgroundAlert() {
- isAlertScheduled = true
- // Force execution to bypass throttle when starting
- scheduleBackgroundAlert(force: true)
- }
- /// Stop all scheduled background alerts.
- func stopBackgroundAlert() {
- isAlertScheduled = false
- removeDeliveredNotifications()
- cancelBackgroundAlerts()
- }
- /// (Re)schedule all background alerts based on predefined durations.
- /// - Parameter force: When true, the scheduling is executed regardless of throttle constraints.
- func scheduleBackgroundAlert(force: Bool = false) {
- guard isAlertScheduled, Storage.shared.backgroundRefreshType.value != .none else { return }
- // Throttle execution if not forced: only run once every 10 seconds.
- if !force {
- let now = Date()
- if let lastDate = lastScheduleDate, now.timeIntervalSince(lastDate) < 10 {
- return
- }
- lastScheduleDate = now
- }
- removeDeliveredNotifications()
- let isBluetoothActive = Storage.shared.backgroundRefreshType.value.isBluetooth
- let expectedHeartbeat = BLEManager.shared.expectedHeartbeatInterval()
- // Define alerts
- let alerts: [BackgroundAlert] = [
- BackgroundAlert(
- identifier: BackgroundAlertIdentifier.sixMin.rawValue,
- timeInterval: BackgroundAlertDuration.sixMinutes.rawValue,
- body: isBluetoothActive
- ? "App inactive for 6 minutes. Verify Bluetooth connectivity."
- : "App inactive for 6 minutes. Open to resume."
- ),
- BackgroundAlert(
- identifier: BackgroundAlertIdentifier.twelveMin.rawValue,
- timeInterval: BackgroundAlertDuration.twelveMinutes.rawValue,
- body: isBluetoothActive
- ? "App inactive for 12 minutes. Verify Bluetooth connectivity."
- : "App inactive for 12 minutes. Open to resume."
- ),
- BackgroundAlert(
- identifier: BackgroundAlertIdentifier.eighteenMin.rawValue,
- timeInterval: BackgroundAlertDuration.eighteenMinutes.rawValue,
- body: isBluetoothActive
- ? "App inactive for 18 minutes. Verify Bluetooth connectivity."
- : "App inactive for 18 minutes. Open to resume."
- ),
- ]
- for alert in alerts {
- // Skip if the expected heartbeat interval matches or exceeds 1.2x the alert time interval
- if let heartbeat = expectedHeartbeat, heartbeat * 1.2 >= alert.timeInterval {
- continue
- }
- let content = createNotificationContent(for: notificationTitlePrefix, body: alert.body)
- let trigger = UNTimeIntervalNotificationTrigger(timeInterval: alert.timeInterval, repeats: false)
- let request = UNNotificationRequest(identifier: alert.identifier, content: content, trigger: trigger)
- UNUserNotificationCenter.current().add(request) { error in
- if let error = error {
- LogManager.shared.log(category: .general, message: "Error scheduling \(alert.timeInterval / 60)-minute background alert: \(error)")
- }
- }
- }
- }
- /// Create notification content with a given title and body.
- /// - Parameters:
- /// - title: The title of the notification.
- /// - body: The body text of the notification.
- /// - Returns: Configured `UNMutableNotificationContent` object.
- private func createNotificationContent(for title: String, body: String) -> UNMutableNotificationContent {
- let content = UNMutableNotificationContent()
- content.title = title
- content.body = body
- content.sound = .defaultCritical
- content.categoryIdentifier = BackgroundAlertIdentifier.categoryIdentifier
- return content
- }
- /// Cancel all scheduled background alerts.
- private func cancelBackgroundAlerts() {
- let identifiers = BackgroundAlertIdentifier.allCases.map { $0.rawValue }
- UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
- }
- /// Remove all delivered notifications.
- private func removeDeliveredNotifications() {
- let identifiers = BackgroundAlertIdentifier.allCases.map { $0.rawValue }
- UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
- }
- }
- /// Struct representing a single background alert.
- struct BackgroundAlert {
- let identifier: String
- let timeInterval: TimeInterval
- let body: String
- }
|