// // BackgroundTaskBridge.swift // OutLive // // Native bridge responsible for scheduling and relaying iOS background tasks to the JS runtime. // import BackgroundTasks import Foundation import React import UIKit @objc(BackgroundTaskBridge) class BackgroundTaskBridge: RCTEventEmitter { private enum TaskKind: String { case processing case refresh } private let queue = DispatchQueue(label: "com.anonymous.digitalpilates.background.bridge", qos: .utility) private var hasListeners = false private var identifier: String? private var kind: TaskKind = .processing private var requiresNetworkConnectivity = false private var requiresExternalPower = false private var defaultDelay: TimeInterval = 60 * 30 // 30 minutes private var currentTask: BGTask? override init() { super.init() // 监听来自 AppDelegate 的后台任务通知 NotificationCenter.default.addObserver( self, selector: #selector(handleTaskNotification(_:)), name: NSNotification.Name("BackgroundTaskBridge.handleTask"), object: nil ) } deinit { NotificationCenter.default.removeObserver(self) } override static func requiresMainQueueSetup() -> Bool { true } override func startObserving() { hasListeners = true } override func stopObserving() { hasListeners = false } @objc private func handleTaskNotification(_ notification: Notification) { guard #available(iOS 13.0, *), let userInfo = notification.userInfo, let task = userInfo["task"] as? BGTask else { return } NSLog("[BackgroundTaskBridge] 收到来自 AppDelegate 的后台任务") handle(task: task) } override func supportedEvents() -> [String]! { [ "BackgroundTaskBridge.execute", "BackgroundTaskBridge.expire" ] } @objc func configure( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard #available(iOS 13.0, *) else { rejecter( "BACKGROUND_TASK_UNAVAILABLE", "BGTaskScheduler requires iOS 13 or later.", nil ) return } guard let identifier = options["identifier"] as? String, !identifier.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { rejecter("INVALID_IDENTIFIER", "A non-empty identifier is required.", nil) return } let taskTypeRaw = (options["taskType"] as? String)?.lowercased() let taskKind = TaskKind(rawValue: taskTypeRaw ?? "") ?? .processing let requiresNetwork = (options["requiresNetworkConnectivity"] as? Bool) ?? false let requiresPower = (options["requiresExternalPower"] as? Bool) ?? false let defaultDelaySeconds = (options["defaultDelay"] as? NSNumber)?.doubleValue queue.async { [weak self] in guard let self else { return } self.identifier = identifier self.kind = taskKind self.requiresNetworkConnectivity = requiresNetwork self.requiresExternalPower = requiresPower if let delay = defaultDelaySeconds, delay > 0 { self.defaultDelay = delay } do { try self.scheduleTask(after: self.defaultDelay) resolver([ "identifier": identifier, "scheduled": true, "defaultDelay": self.defaultDelay ]) } catch { rejecter("BACKGROUND_TASK_ERROR", error.localizedDescription, error) } } } @objc func schedule( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard #available(iOS 13.0, *) else { rejecter( "BACKGROUND_TASK_UNAVAILABLE", "BGTaskScheduler requires iOS 13 or later.", nil ) return } guard let identifier = identifier else { rejecter("NOT_CONFIGURED", "Call configure() before scheduling.", nil) return } let delay = (options["delay"] as? NSNumber)?.doubleValue let effectiveDelay = (delay?.isFinite == true && delay! >= 0) ? delay! : defaultDelay queue.async { [weak self] in guard let self else { return } do { try self.scheduleTask(after: effectiveDelay) resolver([ "identifier": identifier, "scheduled": true, "delay": effectiveDelay ]) } catch { rejecter("BACKGROUND_TASK_ERROR", error.localizedDescription, error) } } } @objc func complete( _ success: NSNumber, rescheduleAfter: NSNumber?, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard #available(iOS 13.0, *) else { rejecter( "BACKGROUND_TASK_UNAVAILABLE", "BGTaskScheduler requires iOS 13 or later.", nil ) return } let shouldRescheduleAfter = rescheduleAfter?.doubleValue ?? defaultDelay let completionSuccess = success.boolValue queue.async { [weak self] in guard let self else { return } if let task = self.currentTask { task.setTaskCompleted(success: completionSuccess) self.currentTask = nil } do { try self.scheduleTask(after: shouldRescheduleAfter) resolver([ "rescheduled": true, "delay": shouldRescheduleAfter, "success": completionSuccess ]) } catch { rejecter("BACKGROUND_TASK_ERROR", error.localizedDescription, error) } } } @objc func cancelAll( _ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard #available(iOS 13.0, *) else { rejecter( "BACKGROUND_TASK_UNAVAILABLE", "BGTaskScheduler requires iOS 13 or later.", nil ) return } queue.async { BGTaskScheduler.shared.cancelAllTaskRequests() resolver(["cancelled": true]) } } @objc func getPendingRequests( _ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard #available(iOS 13.0, *) else { rejecter( "BACKGROUND_TASK_UNAVAILABLE", "BGTaskScheduler requires iOS 13 or later.", nil ) return } BGTaskScheduler.shared.getPendingTaskRequests { [weak self] requests in guard let self else { return } let mapped = requests .filter { request in guard let identifier = self.identifier else { return true } return request.identifier == identifier } .map { request -> [String: Any] in var payload: [String: Any] = [ "identifier": request.identifier ] if let date = request.earliestBeginDate { payload["earliestBegin"] = date.timeIntervalSince1970 * 1000 } else { payload["earliestBegin"] = NSNull() } if let processingRequest = request as? BGProcessingTaskRequest { payload["requiresNetworkConnectivity"] = processingRequest.requiresNetworkConnectivity payload["requiresExternalPower"] = processingRequest.requiresExternalPower payload["type"] = "processing" } else { payload["type"] = "refresh" } return payload } resolver(mapped) } } @objc func backgroundRefreshStatus( _ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard Thread.isMainThread else { DispatchQueue.main.async { [weak self] in self?.backgroundRefreshStatus(resolver, rejecter: rejecter) } return } let status = UIApplication.shared.backgroundRefreshStatus switch status { case .available: resolver("available") case .denied: resolver("denied") case .restricted: resolver("restricted") @unknown default: resolver("unknown") } } @objc func simulateLaunch( _ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard hasListeners else { rejecter("NO_LISTENERS", "No JS listeners registered for background events.", nil) return } DispatchQueue.main.async { [weak self] in guard let self else { return } self.sendEvent( withName: "BackgroundTaskBridge.execute", body: [ "identifier": self.identifier ?? "simulated", "timestamp": Date().timeIntervalSince1970 * 1000, "simulated": true ] ) resolver(["simulated": true]) } } // MARK: - Private helpers @available(iOS 13.0, *) private func scheduleTask(after delay: TimeInterval) throws { guard let identifier else { throw NSError( domain: "BackgroundTaskBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Identifier is missing. Configure the bridge first."] ) } // 取消之前的任务请求 BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier) // 使用 BGAppRefreshTaskRequest 而不是 BGProcessingTaskRequest // BGAppRefreshTaskRequest 更适合定期刷新数据的场景 let request = BGAppRefreshTaskRequest(identifier: identifier) // 设置最早开始时间 // 注意:实际执行时间由系统决定,可能会延迟 request.earliestBeginDate = Date(timeIntervalSinceNow: delay) do { try BGTaskScheduler.shared.submit(request) NSLog("[BackgroundTaskBridge] 后台任务已调度,标识符: \(identifier),延迟: \(delay)秒") } catch { NSLog("[BackgroundTaskBridge] 调度后台任务失败: \(error.localizedDescription)") throw error } } @available(iOS 13.0, *) private func handle(task: BGTask) { queue.async { [weak self] in guard let self else { return } self.currentTask = task NSLog("[BackgroundTaskBridge] 开始处理后台任务: \(task.identifier)") // 设置任务过期处理器 task.expirationHandler = { [weak self] in guard let self else { return } NSLog("[BackgroundTaskBridge] 后台任务即将过期") self.queue.async { if let currentTask = self.currentTask { currentTask.setTaskCompleted(success: false) } self.currentTask = nil // 即使任务过期,也要重新调度下一次任务 self.rescheduleTask() } guard self.hasListeners else { return } DispatchQueue.main.async { self.sendEvent( withName: "BackgroundTaskBridge.expire", body: [ "identifier": self.identifier ?? "", "timestamp": Date().timeIntervalSince1970 * 1000 ] ) } } guard self.hasListeners else { NSLog("[BackgroundTaskBridge] 没有JS监听器,执行默认处理并重新调度") // 即使没有JS监听器,也要执行基本的任务处理 self.executeDefaultTaskAndReschedule() return } NSLog("[BackgroundTaskBridge] 发送后台任务执行事件到JS") DispatchQueue.main.async { self.sendEvent( withName: "BackgroundTaskBridge.execute", body: [ "identifier": self.identifier ?? "", "timestamp": Date().timeIntervalSince1970 * 1000 ] ) } } } @available(iOS 13.0, *) private func executeDefaultTaskAndReschedule() { // 执行默认的后台任务逻辑(当没有JS监听器时) NSLog("[BackgroundTaskBridge] 执行默认后台任务逻辑") // 这里可以添加一些基本的任务逻辑,比如: // 1. 检查应用状态 // 2. 更新本地存储 // 3. 准备下次任务的数据 // 完成当前任务 if let currentTask = self.currentTask { currentTask.setTaskCompleted(success: true) self.currentTask = nil } // 重新调度下一次任务 self.rescheduleTask() } @available(iOS 13.0, *) private func rescheduleTask() { guard let identifier = self.identifier else { NSLog("[BackgroundTaskBridge] 无法重新调度任务:标识符为空") return } // 先取消之前的任务请求,避免重复 BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier) let request = BGAppRefreshTaskRequest(identifier: identifier) request.earliestBeginDate = Date(timeIntervalSinceNow: self.defaultDelay) do { try BGTaskScheduler.shared.submit(request) NSLog("[BackgroundTaskBridge] 已重新调度后台任务,标识符: \(identifier),延迟: \(self.defaultDelay)秒") } catch { NSLog("[BackgroundTaskBridge] 重新调度后台任务失败: \(error.localizedDescription)") // 如果调度失败,尝试使用更短的延迟时间重试 let retryDelay = min(self.defaultDelay * 0.5, 5 * 60) // 最多5分钟 request.earliestBeginDate = Date(timeIntervalSinceNow: retryDelay) do { try BGTaskScheduler.shared.submit(request) NSLog("[BackgroundTaskBridge] 使用重试延迟重新调度后台任务成功: \(retryDelay)秒") } catch { NSLog("[BackgroundTaskBridge] 重试调度后台任务也失败: \(error.localizedDescription)") } } } }