jsdw_ios/QuickLocation/Manager/Unlock/UnlockCountManager.swift

745 lines
31 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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)")
}
}