// // 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? private let pendingTaskWaitTimeout: TimeInterval = 20 private var waitingForJSListeners = false private var pendingTaskPayload: [String: Any]? private var pendingTaskTimeoutWorkItem: DispatchWorkItem? 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 queue.async { [weak self] in self?.emitPendingTaskIfPossible() } } 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 ) { #if targetEnvironment(simulator) // 在模拟器上,给出更友好的提示 NSLog("[BackgroundTaskBridge] ⚠️ 后台任务在模拟器上不完全支持") NSLog("[BackgroundTaskBridge] 请在真机上测试后台任务功能") rejecter( "SIMULATOR_NOT_SUPPORTED", "后台任务功能在模拟器上不完全支持。请在真机上测试。\n注意:这不是错误,是 iOS 模拟器的正常限制。", nil ) return #else guard hasListeners else { NSLog("[BackgroundTaskBridge] ⚠️ 没有 JS 监听器注册") rejecter( "NO_LISTENERS", "没有 JS 监听器注册后台事件。请确保应用已完全初始化。", nil ) return } NSLog("[BackgroundTaskBridge] 模拟触发后台任务...") 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 ] ) NSLog("[BackgroundTaskBridge] ✅ 模拟后台任务已触发") resolver(["simulated": true]) } #endif } @objc func markJSReady( _ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { queue.async { [weak self] in guard let self else { return } let hadPendingTask = self.waitingForJSListeners && self.pendingTaskPayload != nil self.emitPendingTaskIfPossible() resolver([ "pendingTaskFlushed": hadPendingTask ]) } } // 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.getPendingTaskRequests { requests in let existingTasks = requests.filter { $0.identifier == identifier } NSLog("[BackgroundTaskBridge] 当前待处理任务数: \(existingTasks.count)") } // 取消之前的任务请求,避免重复调度 BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier) NSLog("[BackgroundTaskBridge] 已取消之前的任务请求: \(identifier)") // 根据任务类型选择合适的请求类 // processing 类型使用 BGProcessingTaskRequest,fetch 类型使用 BGAppRefreshTaskRequest let request: BGTaskRequest switch kind { case .processing: request = BGProcessingTaskRequest(identifier: identifier) case .refresh: request = BGAppRefreshTaskRequest(identifier: identifier) } // 设置最早开始时间 // 注意:实际执行时间由系统决定,可能会延迟 // 系统通常会在设备空闲、网络连接良好、电量充足时执行 if let appRefreshRequest = request as? BGAppRefreshTaskRequest { appRefreshRequest.earliestBeginDate = Date(timeIntervalSinceNow: delay) } else if let processingRequest = request as? BGProcessingTaskRequest { processingRequest.earliestBeginDate = Date(timeIntervalSinceNow: delay) processingRequest.requiresNetworkConnectivity = requiresNetworkConnectivity processingRequest.requiresExternalPower = requiresExternalPower } do { try BGTaskScheduler.shared.submit(request) let dateFormatter = DateFormatter() dateFormatter.dateFormat = "HH:mm:ss" let earliestTime = dateFormatter.string(from: request.earliestBeginDate ?? Date()) NSLog("[BackgroundTaskBridge] ✅ 后台任务已调度成功") NSLog("[BackgroundTaskBridge] - 标识符: \(identifier)") NSLog("[BackgroundTaskBridge] - 延迟: \(Int(delay))秒 (\(Int(delay/60))分钟)") NSLog("[BackgroundTaskBridge] - 最早执行时间: \(earliestTime)") NSLog("[BackgroundTaskBridge] - 注意: 实际执行时间由系统决定") } catch { NSLog("[BackgroundTaskBridge] ❌ 调度后台任务失败: \(error.localizedDescription)") // 打印详细错误信息 if let bgError = error as NSError? { NSLog("[BackgroundTaskBridge] - 错误域: \(bgError.domain)") NSLog("[BackgroundTaskBridge] - 错误码: \(bgError.code)") NSLog("[BackgroundTaskBridge] - 错误信息: \(bgError.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] ===== 开始处理后台任务 =====") NSLog("[BackgroundTaskBridge] 任务标识符: \(task.identifier)") NSLog("[BackgroundTaskBridge] 任务类型: \(type(of: task))") NSLog("[BackgroundTaskBridge] 当前时间: \(Date().description)") if self.identifier == nil { self.identifier = task.identifier NSLog("[BackgroundTaskBridge] 使用任务标识符初始化 identifier: \(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 ] ) } } let payload: [String: Any] = [ "identifier": self.identifier ?? task.identifier, "timestamp": Date().timeIntervalSince1970 * 1000 ] guard self.hasListeners else { NSLog("[BackgroundTaskBridge] 暂无 JS 监听器,等待 JS 初始化 (最多 \(Int(self.pendingTaskWaitTimeout)) 秒)") self.cacheTaskForLater(payload: payload) return } NSLog("[BackgroundTaskBridge] 准备发送后台任务执行事件到JS") NSLog("[BackgroundTaskBridge] hasListeners: \(self.hasListeners)") self.emitTaskToJS(payload: payload) NSLog("[BackgroundTaskBridge] ✅ 事件已发送到JS层") } } @available(iOS 13.0, *) private func executeDefaultTaskAndReschedule() { waitingForJSListeners = false pendingTaskPayload = nil pendingTaskTimeoutWorkItem?.cancel() pendingTaskTimeoutWorkItem = nil // 执行默认的后台任务逻辑(当没有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: BGTaskRequest switch self.kind { case .processing: request = BGProcessingTaskRequest(identifier: identifier) if let processingRequest = request as? BGProcessingTaskRequest { processingRequest.earliestBeginDate = Date(timeIntervalSinceNow: self.defaultDelay) processingRequest.requiresNetworkConnectivity = self.requiresNetworkConnectivity processingRequest.requiresExternalPower = self.requiresExternalPower } case .refresh: request = BGAppRefreshTaskRequest(identifier: identifier) if let appRefreshRequest = request as? BGAppRefreshTaskRequest { appRefreshRequest.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)") } } } private func cacheTaskForLater(payload: [String: Any]) { waitingForJSListeners = true pendingTaskPayload = payload pendingTaskTimeoutWorkItem?.cancel() NSLog("[BackgroundTaskBridge] 缓存后台任务等待JS监听器...") NSLog("[BackgroundTaskBridge] 等待超时时间: \(Int(pendingTaskWaitTimeout))秒") let timeoutItem = DispatchWorkItem { [weak self] in guard let self else { return } guard self.waitingForJSListeners else { return } NSLog("[BackgroundTaskBridge] ⚠️ 等待 JS 监听器超时,执行默认处理") self.waitingForJSListeners = false self.pendingTaskPayload = nil self.pendingTaskTimeoutWorkItem = nil self.executeDefaultTaskAndReschedule() } pendingTaskTimeoutWorkItem = timeoutItem queue.asyncAfter(deadline: .now() + pendingTaskWaitTimeout, execute: timeoutItem) NSLog("[BackgroundTaskBridge] 已设置超时计时器") } private func emitPendingTaskIfPossible() { guard hasListeners else { return } guard waitingForJSListeners, let payload = pendingTaskPayload else { return } pendingTaskTimeoutWorkItem?.cancel() pendingTaskTimeoutWorkItem = nil waitingForJSListeners = false pendingTaskPayload = nil NSLog("[BackgroundTaskBridge] JS 监听器已就绪,发送缓存的后台任务事件") emitTaskToJS(payload: payload) } private func emitTaskToJS(payload: [String: Any]) { DispatchQueue.main.async { [weak self] in guard let self else { return } NSLog("[BackgroundTaskBridge] 正在发送事件到JS层...") self.sendEvent( withName: "BackgroundTaskBridge.execute", body: payload ) NSLog("[BackgroundTaskBridge] ✅ 事件发送成功") } } }