Files
photo-live/apps/mobile/ios/Runner/UploadPlugin.swift
T

532 lines
21 KiB
Swift
Raw Normal View History

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<Int> = []
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()
}
}