// // 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 isRegistered = false 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.registerTaskIfNeeded(identifier: identifier) 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.registerTaskIfNeeded(identifier: identifier) 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 registerTaskIfNeeded(identifier: String) throws { guard !isRegistered else { return } // 注意:任务标识符已在 AppDelegate 中注册 // BGTaskScheduler 要求所有任务必须在应用启动完成前注册 // 这里只标记为已注册,实际的任务处理将通过 AppDelegate 中的注册生效 isRegistered = true NSLog("[BackgroundTaskBridge] 使用 AppDelegate 中预注册的后台任务: \(identifier)") } @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."] ) } let request: BGTaskRequest switch kind { case .processing: let processing = BGProcessingTaskRequest(identifier: identifier) processing.requiresNetworkConnectivity = requiresNetworkConnectivity processing.requiresExternalPower = requiresExternalPower processing.earliestBeginDate = Date(timeIntervalSinceNow: delay) request = processing case .refresh: let refresh = BGAppRefreshTaskRequest(identifier: identifier) refresh.earliestBeginDate = Date(timeIntervalSinceNow: delay) request = refresh } do { try BGTaskScheduler.shared.submit(request) } catch { throw error } } @available(iOS 13.0, *) private func handle(task: BGTask) { queue.async { [weak self] in guard let self else { return } self.currentTask = task task.expirationHandler = { [weak self] in guard let self else { return } self.queue.async { self.currentTask = nil } 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 { task.setTaskCompleted(success: false) self.currentTask = nil return } DispatchQueue.main.async { self.sendEvent( withName: "BackgroundTaskBridge.execute", body: [ "identifier": self.identifier ?? "", "timestamp": Date().timeIntervalSince1970 * 1000 ] ) } } } }