import Flutter import Foundation /// 上传桥接插件(P3 计划 Task 8;协议与 Android `UploadPlugin.kt` 逐字对齐)。 /// /// - MethodChannel `photolive/uploads`:registerTask(入队后台 URLSession /// upload task)/ cancelTask / queryTask / ackEvents; /// - EventChannel `photolive/uploads/events`:[UploadEventBus] 终态/进度 /// 事件(终态未 ack 缓存重放 + **跨进程落盘**,见总线文档); /// - 实现 = URLSession background(identifier `com.photolive.photo_live.uploads`): /// 传输由系统调度,App 退后台/锁屏后继续;完成后系统在后台拉起 App 投递 /// 回调(`handleEventsForBackgroundURLSession` → 重建会话重挂 delegate)。 /// /// **进程死亡语义**:iOS 对后台会话事件会在需要时重新拉起 App(区别于 /// Android WorkManager 自持重跑)——本插件启动即重建会话(`ensureSession`), /// delegate 回调照常入总线;终态已落盘,Dart 引擎下次 onListen 时重放, /// 镜像 Android 的缓存重放语义。用户强制退出后系统不再代跑,重新启动时 /// 由重联 + 重放校准(技术方案 §6.2)。 /// /// 失败分类(didCompleteWithError,镜像 Kotlin `UploadPutExecutor`): /// 2xx → completed(带 ETag);403 → needsRenew;其余 4xx → failed /// HTTP_4XX;5xx / NSURLErrorDomain 网络错误 → 原生侧重试(上限 /// [maxWorkAttempts],指数退避初始 10s——镜像 WorkManager 语义;App 挂起 /// 时退避定时器暂停,唤醒后续跑),耗尽后 failed SERVER_ERROR / NETWORK; /// 源文件缺失(注册时预检)→ failed SOURCE_MISSING。 final class UploadPlugin: NSObject { static let shared = UploadPlugin() /// 传输尝试上限(含首发;对齐 Kotlin UploadWorker.MAX_WORK_ATTEMPTS)。 static let maxWorkAttempts = 5 /// 重试退避初始间隔(秒;指数翻倍,对齐 Android WorkManager 10s 初始)。 static let retryBackoffBaseSeconds: Double = 10 /// 进度事件节流(秒;Android 侧每 64KB 块发事件、通知 500ms 节流—— /// iOS 统一在事件层 500ms 节流,终值(sent==total)必发)。 static let progressThrottleSeconds: Double = 0.5 private let lock = NSLock() private let bus = UploadEventBus.shared let store = UploadTaskStore() private var session: URLSession? private var sessionDelegate: UploadSessionDelegate? /// 运行期内存态(锁内):活动系统任务 / 进度快照 / 静默取消标记 / /// 进度节流时间戳 / 系统后台回调。 private var activeTasks: [Int: URLSessionTask] = [:] private var lastProgress: [String: (sent: Int64, total: Int64)] = [:] private var quietCancelIdentifiers: Set = [] private var progressEmittedAt: [String: Double] = [:] private var backgroundCompletionHandler: (() -> Void)? // ── 通道注册(AppDelegate 调用) ───────────────────────────────── static func register(with messenger: FlutterBinaryMessenger) { FlutterMethodChannel( name: UploadBridge.methodChannel, binaryMessenger: messenger ) .setMethodCallHandler { call, result in UploadPlugin.shared.handleMethodCall(call, result: result) } FlutterEventChannel( name: UploadBridge.eventChannel, binaryMessenger: messenger ) .setStreamHandler(UploadEventStreamHandler()) } /// 后台会话重建(启动 / 系统后台拉起时调用;幂等)。 func ensureSession() { lock.lock() guard session == nil else { lock.unlock() return } let config = URLSessionConfiguration.background( withIdentifier: UploadBridge.sessionIdentifier ) config.isDiscretionary = false config.sessionSendsLaunchEvents = true // 无网时挂起等待而非立即失败(镜像 Android NetworkType.CONNECTED 约束)。 config.waitsForConnectivity = true let delegate = UploadSessionDelegate(plugin: self) sessionDelegate = delegate session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil) lock.unlock() } /// 系统后台拉起回调暂存(urlSessionDidFinishEvents 时消费)。 func storeBackgroundCompletionHandler(_ handler: @escaping () -> Void) { lock.lock() backgroundCompletionHandler = handler lock.unlock() } func finishBackgroundEvents() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.lock.lock() let handler = self.backgroundCompletionHandler self.backgroundCompletionHandler = nil self.lock.unlock() handler?() } } // ── MethodChannel(镜像 Kotlin handleMethodCall) ──────────────── private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case UploadBridge.methodRegister: handleRegister(call, result: result) case UploadBridge.methodCancel: handleCancel(call, result: result) case UploadBridge.methodQuery: handleQuery(call, result: result) case UploadBridge.methodAck: handleAck(call, result: result) default: result(FlutterMethodNotImplemented) } } private func handleRegister(_ call: FlutterMethodCall, result: FlutterResult) { guard let raw = call.arguments as? [String: Any] else { result(FlutterError(code: "bad_args", message: "arguments must be a Map", details: nil)) return } do { let spec = try parseRegisterSpec(raw) // 重注册(renew 换 URL / 崩溃恢复重驱):作废旧缓存终态 + // REPLACE 旧系统任务(防同任务双跑;镜像 Kotlin unique work REPLACE)。 bus.purgeTask(taskId: spec.taskId) try startTask(spec: spec) result(true) } catch let error as UploadBridgeBadArgsError { result(FlutterError(code: "bad_args", message: error.message, details: nil)) } catch { result(FlutterError(code: "enqueue_failed", message: "\(error)", details: nil)) } } private func handleCancel(_ call: FlutterMethodCall, result: FlutterResult) { guard let taskId = taskChannelArg(call, key: "taskId") as? String else { result(FlutterError(code: "bad_args", message: "missing 'taskId'", details: nil)) return } cancelQuietly(taskId: taskId) bus.purgeTask(taskId: taskId) result(true) } private func handleQuery(_ call: FlutterMethodCall, result: FlutterResult) { guard let taskId = taskChannelArg(call, key: "taskId") as? String else { result(FlutterError(code: "bad_args", message: "missing 'taskId'", details: nil)) return } guard let entry = store.entry(taskId: taskId) else { // 无映射(未注册 / 已终态清理):镜像 Kotlin 无 WorkInfo 的 null。 result(nil) return } lock.lock() let task = activeTasks[entry.sessionTaskIdentifier] let progress = lastProgress[taskId] lock.unlock() let state: String if let task = task { switch task.state { case .running: state = "RUNNING" case .suspended: state = "ENQUEUED" case .canceling: state = "CANCELLED" case .completed: state = "SUCCEEDED" @unknown default: state = "ENQUEUED" } } else { // 未重挂(进程重启后):系统仍持有任务,等同等待系统调度。 state = "ENQUEUED" } result([ "state": state, "attempt": entry.executions, "progress": progress.map { ["sent": $0.sent, "total": $0.total] }, ]) } private func handleAck(_ call: FlutterMethodCall, result: FlutterResult) { let seq: Int? if let v = taskChannelArg(call, key: "seq") as? NSNumber { seq = v.intValue } else if let v = taskChannelArg(call, key: "seq") as? Int { seq = v } else { seq = nil } guard let seq = seq else { result(FlutterError(code: "bad_args", message: "missing or invalid 'seq'", details: nil)) return } bus.ack(seq: seq) result(true) } /// cancelTask/queryTask/ackEvents 的字符串参数取值。 private func taskChannelArg(_ call: FlutterMethodCall, key: String) -> Any? { (call.arguments as? [String: Any])?[key] } // ── 任务生命周期 ───────────────────────────────────────────────── /// 创建并启动后台 upload task(registerTask / 原生重试共用; /// [executions] 为已执行计数——重试路径沿用,注册路径自 1 起)。 private func startTask(spec: UploadRegisterSpec, executions: Int = 1) throws { ensureSession() guard let session = session else { throw NSError( domain: "com.photolive.photo_live.uploads", code: 1, userInfo: [NSLocalizedDescriptionKey: "session unavailable"] ) } // REPLACE:作废旧系统任务(静默取消,不产生终态事件)。 cancelQuietly(taskId: spec.taskId) guard let url = URL(string: spec.url) else { throw UploadBridgeBadArgsError(message: "'url' invalid") } // 源文件缺失预检(镜像 Kotlin UploadPutExecutor 的 File.exists 分类; // 交由 URLSession 才失败会被误归 NETWORK)。 guard FileManager.default.fileExists(atPath: spec.filePath) else { bus.emitFailed(taskId: spec.taskId, code: UploadBridge.failSourceMissing, status: nil) store.remove(taskId: spec.taskId) return } var request = URLRequest(url: url) request.httpMethod = spec.method.isEmpty ? "PUT" : spec.method.uppercased() for (name, value) in spec.headers { // Content-Length 由 uploadTask(fromFile:) 按文件长度自带; // Content-Type URLSession 不会自动设置,原样透传(与 Kotlin // 经 RequestBody.contentType 表达等价)。 if name.lowercased() == "content-length" { continue } request.setValue(value, forHTTPHeaderField: name) } let task = session.uploadTask( with: request, fromFile: URL(fileURLWithPath: spec.filePath) ) store.upsert( taskId: spec.taskId, entry: UploadTaskStore.Entry( url: spec.url, method: request.httpMethod ?? "PUT", headers: spec.headers, filePath: spec.filePath, uploadId: spec.uploadId, attempt: spec.attempt, executions: executions, sessionTaskIdentifier: task.taskIdentifier ) ) lock.lock() activeTasks[task.taskIdentifier] = task lastProgress[spec.taskId] = nil progressEmittedAt[spec.taskId] = nil lock.unlock() task.resume() } /// 静默取消(用户取消 / 重注册替换):无终态事件(镜像 Kotlin 取消语义 /// ——Dart 引擎先行置 cancelled 终态,本侧不发事件)。 private func cancelQuietly(taskId: String) { let identifier = store.entry(taskId: taskId)?.sessionTaskIdentifier ?? -1 if identifier >= 0 { lock.lock() quietCancelIdentifiers.insert(identifier) if let task = activeTasks[identifier] { task.cancel() } lock.unlock() // 未重挂的任务(进程重启后)经系统任务表兜底取消。 session?.getAllTasks { [weak self] tasks in for task in tasks where task.taskIdentifier == identifier { task.cancel() } self?.lock.lock() self?.quietCancelIdentifiers.remove(identifier) self?.lock.unlock() } } store.remove(taskId: taskId) lock.lock() lastProgress[taskId] = nil progressEmittedAt[taskId] = nil lock.unlock() } /// 终态/静默结束后的运行期清理(store 条目由调用方决定去留)。 private func taskRuntimeCleanup(taskId: String, identifier: Int) { lock.lock() activeTasks[identifier] = nil quietCancelIdentifiers.remove(identifier) lastProgress[taskId] = nil progressEmittedAt[taskId] = nil lock.unlock() } // ── delegate 回调入口(UploadSessionDelegate 转发) ────────────── fileprivate func taskId(forSessionTask identifier: Int) -> String? { store.taskId(sessionTaskIdentifier: identifier) } /// 进度节流(500ms;终值必发)。返回是否应发事件。 fileprivate func shouldEmitProgress(taskId: String, sent: Int64, total: Int64) -> Bool { lock.lock() lastProgress[taskId] = (sent, total) let now = Date().timeIntervalSince1970 let last = progressEmittedAt[taskId] ?? 0 let terminalValue = total > 0 && sent >= total if terminalValue || now - last >= UploadPlugin.progressThrottleSeconds { progressEmittedAt[taskId] = now lock.unlock() return true } lock.unlock() return false } /// 静默取消判定(用户取消 / 替换;系统 NSURLErrorCancelled 归同处理)。 fileprivate func isQuietlyCancelled(identifier: Int) -> Bool { lock.lock() defer { lock.unlock() } return quietCancelIdentifiers.contains(identifier) } /// 传输结束分类(didCompleteWithError;分类表见类注释)。 fileprivate func handleTaskCompletion( taskId: String, identifier: Int, response: NSHTTPURLResponse?, error: Error? ) { if isQuietlyCancelled(identifier: identifier) { taskRuntimeCleanup(taskId: taskId, identifier: identifier) return } if let nsError = error as NSError? { if nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled { taskRuntimeCleanup(taskId: taskId, identifier: identifier) return } // 网络层错误(镜像 Kotlin IOException → 可重试网络类)。 handleRetryable(taskId: taskId, identifier: identifier, serverError: false, status: nil) return } guard let status = response?.statusCode else { handleRetryable(taskId: taskId, identifier: identifier, serverError: false, status: nil) return } switch status { case 200...299: bus.emitCompleted(taskId: taskId, etag: response?.value(forHTTPHeaderField: "ETag")) taskRuntimeCleanup(taskId: taskId, identifier: identifier) store.remove(taskId: taskId) case 403: bus.emitNeedsRenew(taskId: taskId) taskRuntimeCleanup(taskId: taskId, identifier: identifier) store.remove(taskId: taskId) case 400...499: bus.emitFailed(taskId: taskId, code: UploadBridge.failHttp4xx, status: status) taskRuntimeCleanup(taskId: taskId, identifier: identifier) store.remove(taskId: taskId) default: handleRetryable(taskId: taskId, identifier: identifier, serverError: true, status: status) } } /// 可重试结局:未达尝试上限 → 指数退避后重建任务;达上限 → 终态事件 /// (SERVER_ERROR / NETWORK;status 语义对齐 Kotlin——5xx 带 status, /// 网络类不带)。退避定时器经主队列 dispatchAfter——App 挂起期间暂停、 /// 唤醒后补跑(系统已按需后台拉起,G3 真机校准点)。 private func handleRetryable( taskId: String, identifier: Int, serverError: Bool, status: Int? ) { taskRuntimeCleanup(taskId: taskId, identifier: identifier) guard let entry = store.entry(taskId: taskId) else { return } if entry.executions >= UploadPlugin.maxWorkAttempts { bus.emitFailed( taskId: taskId, code: serverError ? UploadBridge.failServerError : UploadBridge.failNetwork, status: status ) store.remove(taskId: taskId) return } var next = entry next.executions += 1 store.upsert(taskId: taskId, entry: next) let delay = UploadPlugin.retryBackoffBaseSeconds * pow(2, Double(entry.executions - 1)) DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in self?.recreate(taskId: taskId) } } /// 重试重建(退避到期;期间被取消/重注册则 entry 已变——按 URL 等字段重建)。 private func recreate(taskId: String) { guard let entry = store.entry(taskId: taskId) else { return } let spec = UploadRegisterSpec( taskId: taskId, url: entry.url, method: entry.method, headers: entry.headers, filePath: entry.filePath, uploadId: entry.uploadId, attempt: entry.attempt ) do { try startTask(spec: spec, executions: entry.executions) } catch { bus.emitFailed(taskId: taskId, code: UploadBridge.failNetwork, status: nil) store.remove(taskId: taskId) } } /// delegate 持有任务引用(重挂/取消用)。 fileprivate func trackActiveTask(_ task: URLSessionTask) { guard let taskId = store.taskId(sessionTaskIdentifier: task.taskIdentifier) else { return } store.mutate(taskId: taskId) { entry in entry.sessionTaskIdentifier = task.taskIdentifier } lock.lock() activeTasks[task.taskIdentifier] = task lock.unlock() } } // ── EventChannel 流(终态缓存重放见 UploadEventBus) ──────────────────── /// EventChannel `photolive/uploads/events`:onListen 接管总线 sink(事件 /// 统一主线程投递,镜像 Kotlin mainHandler.post);onCancel 摘除。 private final class UploadEventStreamHandler: NSObject, FlutterStreamHandler { func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { UploadEventBus.shared.attach { event in DispatchQueue.main.async { events(event) } } return nil } func onCancel(withArguments arguments: Any?) -> FlutterError? { UploadEventBus.shared.detach() return nil } } // ── URLSession delegate(系统回调 → 插件分类) ────────────────────────── /// 后台会话 delegate(独立对象 + weak 回指,避免 session↔plugin 环)。 /// 回调在系统串行队列;所有插件入口自带锁。 private final class UploadSessionDelegate: NSObject, URLSessionDataDelegate { weak var plugin: UploadPlugin? init(plugin: UploadPlugin) { self.plugin = plugin } func urlSession( _ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64 ) { guard let plugin = plugin, let taskId = plugin.taskId(forSessionTask: task.taskIdentifier) else { return } plugin.trackActiveTask(task) if plugin.shouldEmitProgress( taskId: taskId, sent: totalBytesSent, total: totalBytesExpectedToSend ) { UploadEventBus.shared.emitProgress( taskId: taskId, sent: totalBytesSent, total: totalBytesExpectedToSend ) } } /// 上传任务本质是 data task:响应体(MinIO XML/空)读取后丢弃,防连接滞留。 func urlSession( _ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void ) { completionHandler(.discard) } func urlSession( _ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error? ) { guard let plugin = plugin, let taskId = plugin.taskId(forSessionTask: task.taskIdentifier) else { return } plugin.handleTaskCompletion( taskId: taskId, identifier: task.taskIdentifier, response: task.response as? NSHTTPURLResponse, error: error ) } func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { plugin?.finishBackgroundEvents() } }