import CoreLocation import Foundation import MapKit /// Captures the "where am I, what time is it" snapshot — used both when a /// note is taken (stamped into the note) and when notes are recalled (fed to /// the relevance ranking). Location is best-effort: denied or unavailable /// location still yields a usable context (time zone, device). @Observable @MainActor final class ContextService { /// Latest context snapshot. Updated by `refresh()`. private(set) var current: CaptureContext = .empty /// Raw location behind `current`, for distance math at recall time. private(set) var location: CLLocation? private(set) var isRefreshing = false private(set) var authorizationDenied = false init() { current.device = Self.deviceName current.timeZoneID = TimeZone.current.identifier } static var deviceName: String { #if os(macOS) "Mac" #else "iPhone" #endif } /// One-shot refresh: grab a current location (asking for when-in-use /// authorization if needed), then reverse-geocode it into place names. /// Bounded — gives up after ~10 seconds rather than spinning. func refresh() async { guard !isRefreshing else { return } isRefreshing = true defer { isRefreshing = false } current.device = Self.deviceName current.timeZoneID = TimeZone.current.identifier guard let location = await acquireLocation() else { return } self.location = location current.latitude = location.coordinate.latitude current.longitude = location.coordinate.longitude current.horizontalAccuracy = location.horizontalAccuracy await reverseGeocode(location) } /// First fix from `CLLocationUpdate.liveUpdates()`, raced against a /// 10-second deadline. `CLServiceSession` holds the when-in-use /// authorization request open for the duration. private func acquireLocation() async -> CLLocation? { // CLServiceSession is unavailable on macOS; there liveUpdates itself // triggers the authorization prompt. #if os(iOS) let session = CLServiceSession(authorization: .whenInUse) defer { session.invalidate() } #endif let fix = Task { for try await update in CLLocationUpdate.liveUpdates() { if update.authorizationDenied || update.authorizationDeniedGlobally { await MainActor.run { self.authorizationDenied = true } return nil as CLLocation? } if let location = update.location { return location } } return nil } let deadline = Task { try? await Task.sleep(for: .seconds(10)) fix.cancel() } defer { deadline.cancel() } let location = (try? await fix.value) ?? nil if location != nil { authorizationDenied = false } return location } private func reverseGeocode(_ location: CLLocation) async { guard let request = MKReverseGeocodingRequest(location: location) else { return } guard let item = try? await request.mapItems.first else { return } current.placeName = item.name current.thoroughfare = item.address?.shortAddress current.locality = item.addressRepresentations?.cityName } }