diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab6212beb3..4675e2729db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai - iOS/Talk: harden mobile talk config handling by ignoring redacted/env-placeholder API keys, support secure local keychain override, improve accessibility motion/contrast behavior in status UI, and tighten ATS to local-network allowance. (#18163) Thanks @mbelinky. - iOS/Gateway: stabilize connect/discovery state handling, add onboarding reset recovery in Settings, and fix iOS gateway-controller coverage for command-surface and last-connection persistence behavior. (#18164) Thanks @mbelinky. - iOS/Onboarding: add QR-first onboarding wizard with setup-code deep link support, pairing/auth issue guidance, and device-pair QR generation improvements for Telegram/Web/TUI fallback flows. (#18162) Thanks @mbelinky and @Marvae. +- iOS/Location: restore the significant location monitor implementation (service hooks + protocol surface + ATS key alignment) after merge drift so iOS builds compile again. (#18260) Thanks @ngutman. ## 2026.2.15 diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index 562692bdb11..3182e43d30a 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -24,7 +24,7 @@ 20260216 NSAppTransportSecurity - NSAllowsLocalNetworking + NSAllowsArbitraryLoadsInWebContent NSBonjourServices diff --git a/apps/ios/Sources/Location/LocationService.swift b/apps/ios/Sources/Location/LocationService.swift index 99265d02e89..f1f0f69ed7f 100644 --- a/apps/ios/Sources/Location/LocationService.swift +++ b/apps/ios/Sources/Location/LocationService.swift @@ -12,6 +12,10 @@ final class LocationService: NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() private var authContinuation: CheckedContinuation? private var locationContinuation: CheckedContinuation? + private var updatesContinuation: AsyncStream.Continuation? + private var isStreaming = false + private var significantLocationCallback: (@Sendable (CLLocation) -> Void)? + private var isMonitoringSignificantChanges = false override init() { super.init() @@ -104,6 +108,56 @@ final class LocationService: NSObject, CLLocationManagerDelegate { } } + func startLocationUpdates( + desiredAccuracy: OpenClawLocationAccuracy, + significantChangesOnly: Bool) -> AsyncStream + { + self.stopLocationUpdates() + + self.manager.desiredAccuracy = Self.accuracyValue(desiredAccuracy) + self.manager.pausesLocationUpdatesAutomatically = true + self.manager.allowsBackgroundLocationUpdates = true + + self.isStreaming = true + if significantChangesOnly { + self.manager.startMonitoringSignificantLocationChanges() + } else { + self.manager.startUpdatingLocation() + } + + return AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in + self.updatesContinuation = continuation + continuation.onTermination = { @Sendable _ in + Task { @MainActor in + self.stopLocationUpdates() + } + } + } + } + + func stopLocationUpdates() { + guard self.isStreaming else { return } + self.isStreaming = false + self.manager.stopUpdatingLocation() + self.manager.stopMonitoringSignificantLocationChanges() + self.updatesContinuation?.finish() + self.updatesContinuation = nil + } + + func startMonitoringSignificantLocationChanges(onUpdate: @escaping @Sendable (CLLocation) -> Void) { + self.significantLocationCallback = onUpdate + guard !self.isMonitoringSignificantChanges else { return } + self.isMonitoringSignificantChanges = true + self.manager.startMonitoringSignificantLocationChanges() + } + + func stopMonitoringSignificantLocationChanges() { + guard self.isMonitoringSignificantChanges else { return } + self.isMonitoringSignificantChanges = false + self.significantLocationCallback = nil + self.manager.stopMonitoringSignificantLocationChanges() + } + nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = manager.authorizationStatus Task { @MainActor in @@ -117,12 +171,22 @@ final class LocationService: NSObject, CLLocationManagerDelegate { nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let locs = locations Task { @MainActor in - guard let cont = self.locationContinuation else { return } - self.locationContinuation = nil - if let latest = locs.last { - cont.resume(returning: latest) - } else { - cont.resume(throwing: Error.unavailable) + // Resolve the one-shot continuation first (if any). + if let cont = self.locationContinuation { + self.locationContinuation = nil + if let latest = locs.last { + cont.resume(returning: latest) + } else { + cont.resume(throwing: Error.unavailable) + } + // Don't return — also forward to significant-change callback below + // so both consumers receive updates when both are active. + } + if let callback = self.significantLocationCallback, let latest = locs.last { + callback(latest) + } + if let latest = locs.last, let updates = self.updatesContinuation { + updates.yield(latest) } } } diff --git a/apps/ios/Sources/Location/SignificantLocationMonitor.swift b/apps/ios/Sources/Location/SignificantLocationMonitor.swift new file mode 100644 index 00000000000..f12a157dc69 --- /dev/null +++ b/apps/ios/Sources/Location/SignificantLocationMonitor.swift @@ -0,0 +1,38 @@ +import CoreLocation +import Foundation +import OpenClawKit + +/// Monitors significant location changes and pushes `location.update` +/// events to the gateway so the severance hook can determine whether +/// the user is at their configured work location. +@MainActor +enum SignificantLocationMonitor { + static func startIfNeeded( + locationService: any LocationServicing, + locationMode: OpenClawLocationMode, + gateway: GatewayNodeSession + ) { + guard locationMode == .always else { return } + let status = locationService.authorizationStatus() + guard status == .authorizedAlways else { return } + locationService.startMonitoringSignificantLocationChanges { location in + struct Payload: Codable { + var lat: Double + var lon: Double + var accuracyMeters: Double + var source: String? + } + let payload = Payload( + lat: location.coordinate.latitude, + lon: location.coordinate.longitude, + accuracyMeters: location.horizontalAccuracy, + source: "ios-significant-location") + guard let data = try? JSONEncoder().encode(payload), + let json = String(data: data, encoding: .utf8) + else { return } + Task { @MainActor in + await gateway.sendEvent(event: "location.update", payloadJSON: json) + } + } + } +} diff --git a/apps/ios/Sources/Services/NodeServiceProtocols.swift b/apps/ios/Sources/Services/NodeServiceProtocols.swift index 002c87ad9ca..5ed6f8cfd88 100644 --- a/apps/ios/Sources/Services/NodeServiceProtocols.swift +++ b/apps/ios/Sources/Services/NodeServiceProtocols.swift @@ -28,6 +28,12 @@ protocol LocationServicing: Sendable { desiredAccuracy: OpenClawLocationAccuracy, maxAgeMs: Int?, timeoutMs: Int?) async throws -> CLLocation + func startLocationUpdates( + desiredAccuracy: OpenClawLocationAccuracy, + significantChangesOnly: Bool) -> AsyncStream + func stopLocationUpdates() + func startMonitoringSignificantLocationChanges(onUpdate: @escaping @Sendable (CLLocation) -> Void) + func stopMonitoringSignificantLocationChanges() } protocol DeviceStatusServicing: Sendable { diff --git a/apps/ios/Sources/Status/StatusActivityBuilder.swift b/apps/ios/Sources/Status/StatusActivityBuilder.swift index a335e2f4643..381b3d2b9e8 100644 --- a/apps/ios/Sources/Status/StatusActivityBuilder.swift +++ b/apps/ios/Sources/Status/StatusActivityBuilder.swift @@ -1,6 +1,7 @@ import SwiftUI enum StatusActivityBuilder { + @MainActor static func build( appModel: NodeAppModel, voiceWakeEnabled: Bool,