import BackgroundTasks import UIKit // 后台续传(iOS 26 BGContinuedProcessingTask)。前台发起的传输切到后台时,向系统申请续跑 // 任务并展示系统进度 UI(类比 AirDrop)。任务生命周期绑定活跃传输集合:0→>0(且前台)时 // 提交、随字节更新 progress、回到 0 时完成。 // // 真机验证点(R-iOS-3):BGContinuedProcessingTask 给「应用」后台运行时间 + 系统进度 UI, // 但承载传输的 WKWebView 是独立 WebContent 进程,其 JS / WebRTC 是否随之保活须真机验;若仍 // 被挂起,行为退化为「切后台暂停、回前台续传」(R-iOS-1 允许,文件传输本需前台)。 // // 全程主线程驱动:register 用 .main 队列、sync 由引擎主线程回调触发,故不引入 actor 隔离。 final class BackgroundTaskManager { static let shared = BackgroundTaskManager() private init() {} // 标识符通配前缀。Info.plist 的 BGTaskSchedulerPermittedIdentifiers 须含 ".*", // 否则注册返回 false、提交抛错——届时仅退化为不申请后台续跑,传输逻辑不受影响。 private static let prefix = "net.commilitia.Commilitia-Drop.transfer" private static let wildcard = prefix + ".*" private var triedRegister = false private var registerOK = false private var task: BGContinuedProcessingTask? private var pendingId: String? // 已提交、尚未 attach 的请求标识符(用于回到 0 时取消)。 private var latestFraction: Double = 0 // 应用启动时注册一次(须在 launch 完成前——由 AppDelegate.didFinishLaunching 调用)。 func registerOnce() { guard !triedRegister else { return } triedRegister = true registerOK = BGTaskScheduler.shared.register( forTaskWithIdentifier: Self.wildcard, using: .main) { [weak self] task in guard let cpt = task as? BGContinuedProcessingTask else { task.setTaskCompleted(success: false); return } self?.attach(cpt) } } // 引擎每次刷新活跃传输时调用:activeCount 活跃传输数,fraction 总进度 [0, 1]。 func sync(activeCount: Int, fraction: Double) { latestFraction = fraction if activeCount > 0 { if let task { task.progress.completedUnitCount = Int64(fraction * 100) } else { submitIfForeground() } } else { finish(success: true) } } private func submitIfForeground() { // 注册失败(未声明权限)/ 已在运行 / 已提交待 attach:均不再提交。 guard registerOK, task == nil, pendingId == nil else { return } // 系统拒绝后台提交(且传输入口本就在前台),故仅前台提交。 guard UIApplication.shared.applicationState != .background else { return } let id = "\(Self.prefix).\(UUID().uuidString.prefix(8))" let request = BGContinuedProcessingTaskRequest( identifier: id, title: t("ios.bg.title"), subtitle: t("ios.bg.subtitle")) request.strategy = .queue do { try BGTaskScheduler.shared.submit(request) pendingId = id } catch { // 提交失败(无空位 / 系统负载高):传输仍在前台正常跑,非致命。 } } private func attach(_ task: BGContinuedProcessingTask) { pendingId = nil self.task = task task.progress.totalUnitCount = 100 task.progress.completedUnitCount = Int64(latestFraction * 100) task.expirationHandler = { [weak self] in self?.task?.setTaskCompleted(success: false) self?.task = nil } } private func finish(success: Bool) { if let task { task.progress.completedUnitCount = task.progress.totalUnitCount task.setTaskCompleted(success: success) self.task = nil } if let pendingId { BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: pendingId) self.pendingId = nil } } }