diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index b592fd2..9bd57b2 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -395,6 +395,50 @@ final class RemoteStore: ObservableObject { #endif } + /// Handle the host's silent "adopt" wake (UX_IOS §5.3): a session started while the app was + /// closed, the host push-*started* the Live Activity, and now the phone must come online long + /// enough to harvest that activity's per-activity update token and register it — so live updates + /// (and a clean end) resume without the user ever opening the app. Called from `PushAppDelegate` + /// on a `content-available` background push, which may relaunch the app with no scene, so this + /// does the setup `onAppear` normally would. Holds a background-task assertion until the token is + /// registered with a host or the window closes, then calls `completion`. Best-effort: iOS may + /// deny background runtime (budget, force-quit), in which case the glance keeps its start-time + /// state until the app next runs. + func handleLiveActivityAdoptWake(completion: @escaping () -> Void) { + guard !demoMode, isPaired else { completion(); return } + setupLiveActivityBridge() // starts the push-to-start + adoption observers if not already + startNetworkingIfNeeded() + #if canImport(UIKit) + var bgTask: UIBackgroundTaskIdentifier = .invalid + var finished = false + let finish = { + guard !finished else { return } + finished = true + if bgTask != .invalid { + UIApplication.shared.endBackgroundTask(bgTask) + bgTask = .invalid + } + completion() + } + bgTask = UIApplication.shared.beginBackgroundTask(withName: "nucleic.liveactivity.adopt") { + finish() // the OS reclaimed the grant before we finished — report what we have + } + #else + let finish = completion + #endif + pathMonitor.refresh() + reconnect() + // Poll until the harvested update token has reached a host (`liveActivitySentTo` fills in via + // the adopt → onPushToken → syncLiveActivityRegistration chain), or we run out of runtime. + Task { @MainActor [weak self] in + for _ in 0..<50 { // ~25s at a 0.5s cadence, inside iOS's ~30s background grant + guard let self, self.liveActivitySentTo.isEmpty else { break } + try? await Task.sleep(for: .milliseconds(500)) + } + finish() + } + } + private func seedDemo() { connectivity = .connected(.lan) hostName = "Andrew's Mac"