// // UnlockCountManager.swift // QuickLocation // import UIKit import CoreLocation import SwiftyUserDefaults /// 本机解锁次数:protectedData 状态机 + 始终定位保持后台运行窗口(不上报) final class UnlockCountManager: NSObject { static let shared = UnlockCountManager() private let locationManager = CLLocationManager() private let coarseLocationManager = CLLocationManager() private let logPrefix = "[DEBUG-UNLOCK]" private let debugLogDefaultsKey = "unlockDebugLogs" private let geofenceIdPrefix = "unlock.keepAlive." private let geofenceCenterId = "unlock.keepAlive.center" private let geofenceRadius: CLLocationDistance = 50 /// 四向卫星围栏相对中心的偏移(米),用于静止时 GPS 抖动触发 private let geofenceSatelliteOffset: CLLocationDistance = 40 /// 相对围栏中心移动超过该距离才刷新围栏网格,避免频繁重注册 private let geofenceRefreshDistance: CLLocationDistance = 40 private var didStart = false private var isSessionLocked = false private var hasSeenLockInSession = false private var protectedDataPollTimer: Timer? private var lastGeofenceCenter: CLLocationCoordinate2D? private var lastKnownLocation: CLLocation? private var bootBackgroundTask: UIBackgroundTaskIdentifier = .invalid /// 当前解锁 session 起点(锁屏结算屏幕时长) private var screenSessionStart: Date? private override init() { super.init() locationManager.delegate = self configureLocationManager( locationManager, accuracy: kCLLocationAccuracyNearestTenMeters, distanceFilter: kCLDistanceFilterNone ) coarseLocationManager.delegate = self configureLocationManager( coarseLocationManager, accuracy: 800, distanceFilter: 100 ) } private func configureLocationManager( _ manager: CLLocationManager, accuracy: CLLocationAccuracy, distanceFilter: CLLocationDistance ) { manager.desiredAccuracy = accuracy manager.distanceFilter = distanceFilter manager.pausesLocationUpdatesAutomatically = false manager.activityType = .otherNavigation if #available(iOS 11.0, *) { manager.showsBackgroundLocationIndicator = false } } // MARK: - Public var todayCount: Int { rollDayIfNeeded() return Defaults[\.unlockCountToday] } var totalCount: Int { rollDayIfNeeded() return Defaults[\.unlockCountTotal] } /// 今日屏幕使用时长(秒),含当前解锁 session 的今日部分 var todayScreenTimeSeconds: Int { rollDayIfNeeded() let today = todayString() var total = Defaults[\.screenTimeByDay][today] ?? 0 if let start = effectiveScreenSessionStart { total += screenTimeSeconds(from: start, to: Date(), onDay: today) } return total } /// 本 App 今日进入前台次数 var todayAppUsageCount: Int { rollDayIfNeeded() return Defaults[\.appUsageToday] } var hasAlwaysLocationAuthorization: Bool { authorizationStatus() == .authorizedAlways } func start(launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) { let appState = UIApplication.shared.applicationState let launchedByLocation = launchOptions?[.location] != nil let nonActiveLaunch = appState != .active if nonActiveLaunch { beginBootBackgroundTask() debugLog("coldBootNonActive appState=\(appState.rawValue) locationKey=\(launchedByLocation) bgMode=\(isLocationBackgroundModeEnabled) auth=\(authorizationStatus().rawValue) bootTask=1") } guard !didStart else { restartLocationMonitoringIfPossible(reassertContinuous: true) handleNonActiveLaunchIfNeeded(launchOptions) return } didStart = true rollDayIfNeeded() restoreScreenSessionIfNeeded() let available = UIApplication.shared.isProtectedDataAvailable isSessionLocked = !available dumpStoredDebugLogs() debugLog("start auth=\(authorizationStatus().rawValue) appState=\(appState.rawValue) protected=\(available) bgMode=\(isLocationBackgroundModeEnabled)") // 冷启动已解锁:不计数,只同步状态 if !available { hasSeenLockInSession = true Defaults[\.unlockWasLocked] = true } NotificationCenter.default.addObserver( self, selector: #selector(protectedDataWillBecomeUnavailable), name: UIApplication.protectedDataWillBecomeUnavailableNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(protectedDataDidBecomeAvailable), name: UIApplication.protectedDataDidBecomeAvailableNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil ) restartLocationMonitoringIfPossible(reassertContinuous: true) handleNonActiveLaunchIfNeeded(launchOptions) reconcileUnlockIfNeeded(reason: "coldStartAfterLock") if UIApplication.shared.isProtectedDataAvailable, !isSessionLocked { startScreenSession() } Defaults[\.unlockLastHeartbeatAt] = Date() } func refreshAuthorizationAndMonitoring() { restartLocationMonitoringIfPossible(reassertContinuous: true) debugLog("refresh auth=\(authorizationStatus().rawValue) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable)") notifyChange() } // MARK: - Day roll private func todayString() -> String { let formatter = DateFormatter() formatter.calendar = Calendar.current formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone.current formatter.dateFormat = "yyyy-MM-dd" return formatter.string(from: Date()) } @discardableResult private func rollDayIfNeeded() -> Bool { migrateScreenTimeStorageIfNeeded() let today = todayString() guard Defaults[\.unlockCountDate] != today else { return false } flushScreenSession() Defaults[\.unlockCountDate] = today Defaults[\.unlockCountToday] = 0 Defaults[\.appUsageToday] = 0 return true } private var effectiveScreenSessionStart: Date? { screenSessionStart ?? Defaults[\.screenSessionStartAt] } private func dayString(for date: Date) -> String { let formatter = DateFormatter() formatter.calendar = Calendar.current formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone.current formatter.dateFormat = "yyyy-MM-dd" return formatter.string(from: date) } private func migrateScreenTimeStorageIfNeeded() { var byDay = Defaults[\.screenTimeByDay] guard byDay.isEmpty else { return } let legacyKey = "screenTimeTodaySeconds" let legacyToday = UserDefaults.standard.integer(forKey: legacyKey) if legacyToday > 0 { let dateKey = Defaults[\.unlockCountDate].isEmpty ? todayString() : Defaults[\.unlockCountDate] byDay[dateKey] = legacyToday Defaults[\.screenTimeByDay] = byDay } } private func addScreenTime(from start: Date, to end: Date) { guard end > start else { return } var cursor = start let calendar = Calendar.current var byDay = Defaults[\.screenTimeByDay] while cursor < end { let dayStart = calendar.startOfDay(for: cursor) guard let nextDay = calendar.date(byAdding: .day, value: 1, to: dayStart) else { break } let segmentEnd = min(end, nextDay) let seconds = max(0, Int(segmentEnd.timeIntervalSince(cursor))) if seconds > 0 { let key = dayString(for: cursor) byDay[key, default: 0] += seconds } cursor = segmentEnd } Defaults[\.screenTimeByDay] = byDay } private func screenTimeSeconds(from start: Date, to end: Date, onDay dayKey: String) -> Int { guard end > start else { return 0 } let calendar = Calendar.current guard let dayStart = date(fromDayKey: dayKey), let nextDay = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 } let segmentStart = max(start, dayStart) let segmentEnd = min(end, nextDay) guard segmentEnd > segmentStart else { return 0 } return max(0, Int(segmentEnd.timeIntervalSince(segmentStart))) } private func date(fromDayKey key: String) -> Date? { let formatter = DateFormatter() formatter.calendar = Calendar.current formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone.current formatter.dateFormat = "yyyy-MM-dd" return formatter.date(from: key) } private func restoreScreenSessionIfNeeded() { guard UIApplication.shared.isProtectedDataAvailable else { return } guard screenSessionStart == nil, let persisted = Defaults[\.screenSessionStartAt] else { return } screenSessionStart = persisted } private func startScreenSession() { guard UIApplication.shared.isProtectedDataAvailable else { return } rollDayIfNeeded() if screenSessionStart == nil { if let persisted = Defaults[\.screenSessionStartAt] { screenSessionStart = persisted } else { screenSessionStart = Date() Defaults[\.screenSessionStartAt] = screenSessionStart } } } private func flushScreenSession() { guard let start = effectiveScreenSessionStart else { return } addScreenTime(from: start, to: Date()) screenSessionStart = nil Defaults[\.screenSessionStartAt] = nil notifyChange() } private func recordAppUsage() { rollDayIfNeeded() Defaults[\.appUsageToday] = Defaults[\.appUsageToday] + 1 notifyChange() debugLog("appUsage today=\(Defaults[\.appUsageToday])") } // MARK: - Counting private func recordUnlock(reason: String) { let now = Date() rollDayIfNeeded() Defaults[\.unlockCountToday] = Defaults[\.unlockCountToday] + 1 Defaults[\.unlockCountTotal] = Defaults[\.unlockCountTotal] + 1 Defaults[\.unlockLastCountedAt] = now Defaults[\.unlockWasLocked] = false isSessionLocked = false Defaults[\.unlockLastHeartbeatAt] = now stopProtectedDataPolling() startScreenSession() notifyChange() print("🔓 UnlockCount +\(reason) today=\(Defaults[\.unlockCountToday]) total=\(Defaults[\.unlockCountTotal])") debugLog("recordUnlock reason=\(reason) today=\(Defaults[\.unlockCountToday]) total=\(Defaults[\.unlockCountTotal])") } private func markLocked() { flushScreenSession() isSessionLocked = true hasSeenLockInSession = true Defaults[\.unlockWasLocked] = true Defaults[\.unlockLastHeartbeatAt] = Date() startProtectedDataPolling() } private func persistLifecycleHeartbeat() { Defaults[\.unlockLastHeartbeatAt] = Date() if !UIApplication.shared.isProtectedDataAvailable { Defaults[\.unlockWasLocked] = true isSessionLocked = true hasSeenLockInSession = true } } private func notifyChange() { NotificationCenter.default.post(name: .unlockCountDidChange, object: nil) } private func debugLog(_ message: String) { let line = "\(Date()) \(logPrefix) \(message)" NSLog("%@", line) var logs = UserDefaults.standard.stringArray(forKey: debugLogDefaultsKey) ?? [] logs.append(line) if logs.count > 200 { logs.removeFirst(logs.count - 200) } UserDefaults.standard.set(logs, forKey: debugLogDefaultsKey) } private func dumpStoredDebugLogs() { let logs = UserDefaults.standard.stringArray(forKey: debugLogDefaultsKey) ?? [] guard !logs.isEmpty else { return } NSLog("%@ storedLogCount=%d", logPrefix, logs.count) logs.suffix(50).forEach { NSLog("%@ stored %@", logPrefix, $0) } } private func startProtectedDataPolling() { guard protectedDataPollTimer == nil else { return } guard Defaults[\.unlockWasLocked] || isSessionLocked else { return } debugLog("startProtectedDataPolling") let timer = Timer(timeInterval: 1, repeats: true) { [weak self] _ in guard let self else { return } self.pollProtectedDataAvailability() } protectedDataPollTimer = timer RunLoop.main.add(timer, forMode: .common) pollProtectedDataAvailability() } private func stopProtectedDataPolling() { guard protectedDataPollTimer != nil else { return } protectedDataPollTimer?.invalidate() protectedDataPollTimer = nil debugLog("stopProtectedDataPolling") } private func pollProtectedDataAvailability() { let available = UIApplication.shared.isProtectedDataAvailable debugLog("protectedDataPoll protected=\(available) wasLocked=\(Defaults[\.unlockWasLocked]) sessionLocked=\(isSessionLocked)") guard available else { return } guard Defaults[\.unlockWasLocked] || isSessionLocked else { stopProtectedDataPolling() return } recordUnlock(reason: "protectedDataPoll") } /// 对照 protectedData 纠正锁态(定位回调补强,避免挂起丢通知) private func syncLockStateFromProtectedData(reason: String) { if !UIApplication.shared.isProtectedDataAvailable { markLocked() return } if Defaults[\.unlockWasLocked] || isSessionLocked { recordUnlock(reason: reason) } } /// 定位事件短窗口:锁态同步 + 心跳 + FinishTask 断言 private func performLocationTick(reason: String) { let app = UIApplication.shared var taskId = UIBackgroundTaskIdentifier.invalid taskId = app.beginBackgroundTask(withName: "unlock.locationTick") { if taskId != .invalid { app.endBackgroundTask(taskId) taskId = .invalid } } syncLockStateFromProtectedData(reason: reason) Defaults[\.unlockLastHeartbeatAt] = Date() if taskId != .invalid { app.endBackgroundTask(taskId) taskId = .invalid } } /// DAS / 定位冷启动短窗口:等 locationd 接管前尽量不被秒挂 private func beginBootBackgroundTask() { endBootBackgroundTask() let app = UIApplication.shared bootBackgroundTask = app.beginBackgroundTask(withName: "unlock.boot") { [weak self] in self?.debugLog("bootTask expired") self?.endBootBackgroundTask() } debugLog("bootTask begin id=\(bootBackgroundTask.rawValue)") DispatchQueue.main.asyncAfter(deadline: .now() + 25) { [weak self] in self?.endBootBackgroundTask() } } private func endBootBackgroundTask() { guard bootBackgroundTask != .invalid else { return } let id = bootBackgroundTask bootBackgroundTask = .invalid UIApplication.shared.endBackgroundTask(id) debugLog("bootTask end id=\(id.rawValue)") } // MARK: - protectedData / lifecycle @objc private func protectedDataWillBecomeUnavailable() { debugLog("protectedDataWillBecomeUnavailable appState=\(UIApplication.shared.applicationState.rawValue)") markLocked() } @objc private func protectedDataDidBecomeAvailable() { debugLog("protectedDataDidBecomeAvailable appState=\(UIApplication.shared.applicationState.rawValue) wasLocked=\(Defaults[\.unlockWasLocked]) sessionLocked=\(isSessionLocked)") guard hasSeenLockInSession || Defaults[\.unlockWasLocked] else { return } guard isSessionLocked || Defaults[\.unlockWasLocked] else { return } recordUnlock(reason: "protectedData") } @objc private func appDidEnterBackground() { persistLifecycleHeartbeat() restartLocationMonitoringIfPossible(reassertContinuous: true) forceRefreshGeofenceFromLastKnown(reason: "didEnterBackground") debugLog("didEnterBackground auth=\(authorizationStatus().rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable) wasLocked=\(Defaults[\.unlockWasLocked]) regions=\(locationManager.monitoredRegions.count)") } @objc private func appWillTerminate() { persistLifecycleHeartbeat() forceRefreshGeofenceFromLastKnown(reason: "willTerminate") debugLog("willTerminate protected=\(UIApplication.shared.isProtectedDataAvailable) wasLocked=\(Defaults[\.unlockWasLocked])") } @objc private func appDidBecomeActive() { debugLog("didBecomeActive protected=\(UIApplication.shared.isProtectedDataAvailable) wasLocked=\(Defaults[\.unlockWasLocked]) sessionLocked=\(isSessionLocked)") endBootBackgroundTask() reconcileUnlockIfNeeded(reason: "becomeActiveAfterLock") recordAppUsage() restartLocationMonitoringIfPossible(reassertContinuous: true) Defaults[\.unlockLastHeartbeatAt] = Date() } /// 杀进程前已记锁屏,解锁后冷启动/回前台时补计 private func reconcileUnlockIfNeeded(reason: String) { guard UIApplication.shared.isProtectedDataAvailable else { return } guard Defaults[\.unlockWasLocked] || isSessionLocked else { return } recordUnlock(reason: reason) } // MARK: - Location wake private func authorizationStatus() -> CLAuthorizationStatus { if #available(iOS 14.0, *) { return locationManager.authorizationStatus } return CLLocationManager.authorizationStatus() } /// Info.plist 的 UIBackgroundModes 必须是含 location 的数组,才可安全打开后台持续定位 private var isLocationBackgroundModeEnabled: Bool { guard let modes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String] else { return false } return modes.contains("location") } private func restartLocationMonitoringIfPossible(reassertContinuous: Bool = false) { guard authorizationStatus() == .authorizedAlways else { stopAllLocationMonitoring() stopProtectedDataPolling() debugLog("stopLocation auth=\(authorizationStatus().rawValue) stopVisits=true stopCoarse=true stopGeofence=true") return } // 重要位置变化 / Visit / 围栏:杀进程后由系统拉起 locationManager.startMonitoringSignificantLocationChanges() locationManager.startMonitoringVisits() debugLog("startVisits auth=\(authorizationStatus().rawValue)") if isLocationBackgroundModeEnabled { reassertContinuousLocationUpdates(forceStopFirst: reassertContinuous) } else { if #available(iOS 9.0, *) { locationManager.allowsBackgroundLocationUpdates = false coarseLocationManager.allowsBackgroundLocationUpdates = false } locationManager.stopUpdatingLocation() coarseLocationManager.stopUpdatingLocation() print("UnlockCountManager: UIBackgroundModes 缺少 location,仅使用重要位置变化/围栏") debugLog("startSignificantOnly auth=\(authorizationStatus().rawValue)") } } private func stopAllLocationMonitoring() { locationManager.stopUpdatingLocation() locationManager.stopMonitoringSignificantLocationChanges() locationManager.stopMonitoringVisits() coarseLocationManager.stopUpdatingLocation() stopGeofenceMonitoring() if #available(iOS 9.0, *) { locationManager.allowsBackgroundLocationUpdates = false coarseLocationManager.allowsBackgroundLocationUpdates = false } } /// 停再开持续定位,迫使 locationd 重新挂上 Location subscription(对齐 Lookus 拉起后行为) private func reassertContinuousLocationUpdates(forceStopFirst: Bool) { guard authorizationStatus() == .authorizedAlways, isLocationBackgroundModeEnabled else { return } if forceStopFirst { locationManager.stopUpdatingLocation() coarseLocationManager.stopUpdatingLocation() debugLog("reassertContinuous stopThenStart") } if #available(iOS 9.0, *) { locationManager.allowsBackgroundLocationUpdates = true coarseLocationManager.allowsBackgroundLocationUpdates = true } locationManager.startUpdatingLocation() coarseLocationManager.startUpdatingLocation() debugLog("startPreciseLocation accuracy=10 distanceFilter=none activity=otherNavigation auth=\(authorizationStatus().rawValue) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable)") debugLog("startCoarseLocation accuracy=800 distanceFilter=100 activity=otherNavigation auth=\(authorizationStatus().rawValue) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable)") } private func stopGeofenceMonitoring() { for region in locationManager.monitoredRegions where region.identifier.hasPrefix(geofenceIdPrefix) { locationManager.stopMonitoring(for: region) } lastGeofenceCenter = nil } /// 中心 + 东南西北共 5 个围栏中心点(偏移约 40m) private func geofenceGridCenters(around coordinate: CLLocationCoordinate2D) -> [(id: String, coordinate: CLLocationCoordinate2D)] { let earthRadius: CLLocationDistance = 6_378_137 let latRad = coordinate.latitude * .pi / 180 let dLat = (geofenceSatelliteOffset / earthRadius) * 180 / .pi let dLon = (geofenceSatelliteOffset / (earthRadius * cos(latRad))) * 180 / .pi return [ (geofenceCenterId, coordinate), ("\(geofenceIdPrefix)n", CLLocationCoordinate2D(latitude: coordinate.latitude + dLat, longitude: coordinate.longitude)), ("\(geofenceIdPrefix)s", CLLocationCoordinate2D(latitude: coordinate.latitude - dLat, longitude: coordinate.longitude)), ("\(geofenceIdPrefix)e", CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: coordinate.longitude + dLon)), ("\(geofenceIdPrefix)w", CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: coordinate.longitude - dLon)) ] } private func rememberLocation(_ location: CLLocation) { lastKnownLocation = location Defaults[\.unlockLastLatitude] = location.coordinate.latitude Defaults[\.unlockLastLongitude] = location.coordinate.longitude } private func resolvedLastKnownLocation() -> CLLocation? { if let location = locationManager.location ?? lastKnownLocation { return location } if let lat = Defaults[\.unlockLastLatitude], let lon = Defaults[\.unlockLastLongitude] { return CLLocation(latitude: lat, longitude: lon) } if let lat = Defaults[\.currentLatitude], let lon = Defaults[\.currentLongitude] { return CLLocation(latitude: lat, longitude: lon) } return nil } private func forceRefreshGeofenceFromLastKnown(reason: String) { guard let location = resolvedLastKnownLocation() else { debugLog("geofenceSkip reason=\(reason) noLastLocation") return } debugLog("geofenceForce reason=\(reason)") refreshGeofence(around: location, force: true) } private func refreshGeofence(around location: CLLocation, force: Bool = false) { guard authorizationStatus() == .authorizedAlways else { return } guard CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) else { debugLog("geofence unavailable") return } let coordinate = location.coordinate if !force, let last = lastGeofenceCenter { let lastLocation = CLLocation(latitude: last.latitude, longitude: last.longitude) if lastLocation.distance(from: location) < geofenceRefreshDistance { return } } stopGeofenceMonitoring() let centers = geofenceGridCenters(around: coordinate) for item in centers { let region = CLCircularRegion( center: item.coordinate, radius: geofenceRadius, identifier: item.id ) region.notifyOnEntry = true region.notifyOnExit = true locationManager.startMonitoring(for: region) } lastGeofenceCenter = coordinate let ours = locationManager.monitoredRegions.filter { $0.identifier.hasPrefix(geofenceIdPrefix) }.count debugLog("geofenceRefresh lat=\(coordinate.latitude) lon=\(coordinate.longitude) radius=\(geofenceRadius) grid=\(centers.count) force=\(force) monitoredOurs=\(ours) monitoredTotal=\(locationManager.monitoredRegions.count)") } /// DAS 预热 / 定位拉起 / 任意非前台冷启动:立刻重申持续定位并尝试恢复围栏 private func handleNonActiveLaunchIfNeeded(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { let launchedByLocation = launchOptions?[.location] != nil let nonActive = UIApplication.shared.applicationState != .active guard launchedByLocation || nonActive else { return } let reason: String if launchedByLocation { reason = "locationLaunch" } else if UIApplication.shared.applicationState == .background { reason = "backgroundLaunch" } else { reason = "inactiveLaunch" } debugLog("handleLaunch reason=\(reason) locationKey=\(launchedByLocation) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable) bgMode=\(isLocationBackgroundModeEnabled)") reassertContinuousLocationUpdates(forceStopFirst: true) forceRefreshGeofenceFromLastKnown(reason: reason) performLocationTick(reason: reason) } } // MARK: - CLLocationManagerDelegate extension UnlockCountManager: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { restartLocationMonitoringIfPossible(reassertContinuous: true) debugLog("authChangedNewAPI auth=\(authorizationStatus().rawValue)") notifyChange() } func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { restartLocationMonitoringIfPossible(reassertContinuous: true) debugLog("authChangedOldAPI status=\(status.rawValue)") notifyChange() } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let source = manager === coarseLocationManager ? "coarse" : "precise" debugLog("didUpdateLocations source=\(source) count=\(locations.count) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable) wasLocked=\(Defaults[\.unlockWasLocked]) sessionLocked=\(isSessionLocked)") performLocationTick(reason: "locationUpdate") if let latest = locations.last { rememberLocation(latest) refreshGeofence(around: latest) } // locationd 已开始送点,可结束 DAS 预热 boot task endBootBackgroundTask() } func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) { debugLog("didVisit arrival=\(visit.arrivalDate) departure=\(visit.departureDate) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable) wasLocked=\(Defaults[\.unlockWasLocked]) sessionLocked=\(isSessionLocked)") beginBootBackgroundTask() reassertContinuousLocationUpdates(forceStopFirst: true) performLocationTick(reason: "visit") if visit.coordinate.latitude != 0 || visit.coordinate.longitude != 0 { let location = CLLocation(latitude: visit.coordinate.latitude, longitude: visit.coordinate.longitude) rememberLocation(location) refreshGeofence(around: location, force: true) } } func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { guard region.identifier.hasPrefix(geofenceIdPrefix) else { return } debugLog("didEnterRegion id=\(region.identifier) appState=\(UIApplication.shared.applicationState.rawValue)") beginBootBackgroundTask() reassertContinuousLocationUpdates(forceStopFirst: true) performLocationTick(reason: "geofenceEnter") } func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { guard region.identifier.hasPrefix(geofenceIdPrefix) else { return } debugLog("didExitRegion id=\(region.identifier) appState=\(UIApplication.shared.applicationState.rawValue)") beginBootBackgroundTask() reassertContinuousLocationUpdates(forceStopFirst: true) performLocationTick(reason: "geofenceExit") if let location = manager.location { rememberLocation(location) refreshGeofence(around: location, force: true) } else { forceRefreshGeofenceFromLastKnown(reason: "geofenceExit") } } func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) { debugLog("geofenceStarted id=\(region.identifier)") } func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) { debugLog("geofenceFail id=\(region?.identifier ?? "nil") error=\(error.localizedDescription)") } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("UnlockCountManager location error: \(error.localizedDescription)") debugLog("locationError=\(error.localizedDescription)") } }