512d7563ee
- 引擎 / 鉴权跨平台 in-place(iOS 路径 #if 守卫、逐字不变,仅加 macOS/AppKit 分支):EngineController(import 守卫 + 剪贴板 UIPasteboard→NSPasteboard 上下行两处 + device_type 平台条件)、EngineWebView(UIViewRepresentable→typealias 统一后分 NSViewRepresentable)、Theme(动态色 Color(nsColor:dynamicProvider) 同 RGB)、DeviceNameStore(Host.current().localizedName)、PushRegistry(NSApplication.registerForRemoteNotifications)、Auth/BrokerLogin(presentationAnchor→NSApplication.keyWindow)、Auth/AuthManager(死 import UIKit 守卫)
- BackgroundTaskManager:整文件 #if os(iOS)(BGContinuedProcessingTask 为 API_UNAVAILABLE(macos)、编译级拒绝)+ macOS 同签名 no-op stub,使 EngineController.syncBackgroundTask 调用点无须改动
- device_type 修正(非机械):Mac 原生客户端 boot 注入 device_type=macos(与桌面 Wails 客户端一致,后端 / 各端已识别),不再硬编 ios——否则其他端设备列表图标错、且触发其 isWakeable 的 contains("ios") 误判本 Mac 可推送唤醒
- CommilitiaDropMac target 纳入「引擎 + 登录」最小集:Shared + Sources/Engine + Sources/Auth(除 iOS 专属 LoginView)+ RecordsStore / PushRegistry / LocalNetworkPermission / DeviceNameStore / BackgroundTaskManager + Resources/i18n;依赖 WebRTC;Info.plist 加 cdrop:// 回调 scheme(ASWebAuthenticationSession OAuth 回调)+ 本地网络(NSLocalNetworkUsageDescription / NSBonjourServices _cdrop._tcp)
- Mac 壳接引擎:CommilitiaDropMacApp(@State auth + engine,MacAppRoot 登录门 + 会话激活分桶记录)、MacLoginView(Broker 账户登录,免相机)、MacRootView(背景挂离屏引擎 WebView 保活 + 设备真实 presence 列表 + 设置账号/登出/引擎日志)、MacMenuContent(账号 + 在线设备数)。传输/文件/消息详情暂占位,随 Phase 3b 复用 iOS 视图
- 验证:just mac-build(CommilitiaDropMac.app)+ iOS 模拟器(CommilitiaDrop.app)均 BUILD SUCCEEDED。运行期登录 / 引擎联通待真机自测(Broker OAuth 需账号交互)
120 lines
4.6 KiB
Swift
120 lines
4.6 KiB
Swift
#if os(iOS)
|
||
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 须含 "<prefix>.*",
|
||
// 否则注册返回 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
|
||
}
|
||
}
|
||
}
|
||
#else
|
||
// macOS 无 BackgroundTasks(BGContinuedProcessingTask 为 API_UNAVAILABLE(macos)):桌面无「切后台
|
||
// 挂起」概念,续传任务无意义。提供同签名 no-op stub,使 EngineController.syncBackgroundTask 等调用点
|
||
// 在 macOS 编译无须改动。
|
||
final class BackgroundTaskManager
|
||
{
|
||
static let shared = BackgroundTaskManager()
|
||
private init() {}
|
||
func registerOnce() {}
|
||
func sync(activeCount: Int, fraction: Double) {}
|
||
}
|
||
#endif
|