mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-22 06:40:44 +08:00
396 lines
16 KiB
Swift
396 lines
16 KiB
Swift
import Combine
|
|
import CoreLocation
|
|
import Foundation
|
|
|
|
@MainActor
|
|
protocol RealtimeLocationDriving: AnyObject {
|
|
var location: CLLocation? { get }
|
|
var authorizationStatus: CLAuthorizationStatus { get }
|
|
var delegate: CLLocationManagerDelegate? { get set }
|
|
func requestWhenInUseAuthorization()
|
|
func requestLocation()
|
|
func startUpdatingLocation()
|
|
func stopUpdatingLocation()
|
|
}
|
|
|
|
@MainActor
|
|
final class CoreLocationDriver: RealtimeLocationDriving {
|
|
private let manager: CLLocationManager
|
|
|
|
init(manager: CLLocationManager = CLLocationManager()) {
|
|
self.manager = manager
|
|
manager.desiredAccuracy = kCLLocationAccuracyBest
|
|
manager.distanceFilter = kCLDistanceFilterNone
|
|
}
|
|
|
|
var location: CLLocation? { manager.location }
|
|
var authorizationStatus: CLAuthorizationStatus { manager.authorizationStatus }
|
|
|
|
var delegate: CLLocationManagerDelegate? {
|
|
get { manager.delegate }
|
|
set { manager.delegate = newValue }
|
|
}
|
|
|
|
func requestWhenInUseAuthorization() { manager.requestWhenInUseAuthorization() }
|
|
func requestLocation() { manager.requestLocation() }
|
|
func startUpdatingLocation() { manager.startUpdatingLocation() }
|
|
func stopUpdatingLocation() { manager.stopUpdatingLocation() }
|
|
}
|
|
|
|
@MainActor
|
|
final class RealtimeLocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
|
|
static let shared = RealtimeLocationManager(driver: CoreLocationDriver())
|
|
|
|
@Published private(set) var location: CLLocation?
|
|
@Published private(set) var authorizationStatus: CLAuthorizationStatus
|
|
@Published private(set) var isRequesting = false
|
|
|
|
private enum RequestPhase: Equatable {
|
|
case awaitingAuthorization
|
|
case oneShot
|
|
case continuousFallback
|
|
}
|
|
|
|
private struct ActiveRequest {
|
|
let id: UInt64
|
|
var startedAt: Date?
|
|
let continuation: CheckedContinuation<CLLocationCoordinate2D?, Never>
|
|
var phase: RequestPhase
|
|
}
|
|
|
|
private let driver: RealtimeLocationDriving
|
|
private let oneShotTimeoutNanoseconds: UInt64
|
|
private let fallbackTimeoutNanoseconds: UInt64
|
|
private let cacheMaxAge: TimeInterval = 20
|
|
private var nextRequestID: UInt64 = 0
|
|
private var activeRequest: ActiveRequest?
|
|
private var timeoutTask: Task<Void, Never>?
|
|
|
|
init(
|
|
driver: RealtimeLocationDriving,
|
|
oneShotTimeoutNanoseconds: UInt64 = 1_500_000_000,
|
|
fallbackTimeoutNanoseconds: UInt64 = 5_000_000_000
|
|
) {
|
|
self.driver = driver
|
|
self.oneShotTimeoutNanoseconds = oneShotTimeoutNanoseconds
|
|
self.fallbackTimeoutNanoseconds = fallbackTimeoutNanoseconds
|
|
authorizationStatus = driver.authorizationStatus
|
|
super.init()
|
|
driver.delegate = self
|
|
if let cached = driver.location, Self.isValid(cached) {
|
|
location = cached
|
|
}
|
|
}
|
|
|
|
convenience init(driver: RealtimeLocationDriving, timeoutNanoseconds: UInt64) {
|
|
self.init(
|
|
driver: driver,
|
|
oneShotTimeoutNanoseconds: timeoutNanoseconds,
|
|
fallbackTimeoutNanoseconds: timeoutNanoseconds
|
|
)
|
|
}
|
|
|
|
func requestLocation() async -> CLLocationCoordinate2D? {
|
|
guard activeRequest == nil else {
|
|
RuntimeLogger.warning("APP", "实时定位", "忽略重复 CLLocationManager 请求", details: [
|
|
"活动requestID": activeRequest.map { String($0.id) } ?? "nil",
|
|
"活动阶段": activeRequest.map { phaseName($0.phase) } ?? "nil"
|
|
])
|
|
return nil
|
|
}
|
|
|
|
authorizationStatus = driver.authorizationStatus
|
|
RuntimeLogger.info("APP", "实时定位", "CLLocationManager 请求入口", details: [
|
|
"授权状态": authorizationName(authorizationStatus),
|
|
"内存缓存存在": String(location != nil),
|
|
"系统缓存存在": String(driver.location != nil)
|
|
])
|
|
guard authorizationStatus != .denied, authorizationStatus != .restricted else {
|
|
RuntimeLogger.warning("APP", "实时定位", "授权状态不允许定位", details: [
|
|
"授权状态": authorizationName(authorizationStatus)
|
|
])
|
|
return nil
|
|
}
|
|
|
|
if let cached = freshestCachedLocation() {
|
|
location = cached
|
|
RealtimeLocationTrace.log("使用 CLLocationManager 新鲜缓存", location: cached, details: [
|
|
"来源": "manager-memory-or-system",
|
|
"缓存上限秒": String(Int(cacheMaxAge))
|
|
])
|
|
return cached.coordinate
|
|
}
|
|
|
|
nextRequestID &+= 1
|
|
let requestID = nextRequestID
|
|
isRequesting = true
|
|
RuntimeLogger.info("APP", "实时定位", "创建 CLLocationManager 请求", details: [
|
|
"requestID": String(requestID),
|
|
"授权状态": authorizationName(authorizationStatus),
|
|
"初始阶段": "awaitingAuthorization"
|
|
])
|
|
|
|
return await withTaskCancellationHandler {
|
|
await withCheckedContinuation { continuation in
|
|
activeRequest = ActiveRequest(
|
|
id: requestID,
|
|
startedAt: nil,
|
|
continuation: continuation,
|
|
phase: .awaitingAuthorization
|
|
)
|
|
|
|
if authorizationStatus == .notDetermined {
|
|
RuntimeLogger.info("APP", "实时定位", "请求前台定位授权", details: [
|
|
"requestID": String(requestID)
|
|
])
|
|
scheduleTimeout(for: requestID, nanoseconds: fallbackTimeoutNanoseconds)
|
|
driver.requestWhenInUseAuthorization()
|
|
} else {
|
|
beginOneShot(for: requestID)
|
|
}
|
|
}
|
|
} onCancel: {
|
|
Task { @MainActor [weak self] in
|
|
self?.finishRequest(id: requestID, coordinate: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func startUpdating() {
|
|
if authorizationStatus == .notDetermined {
|
|
driver.requestWhenInUseAuthorization()
|
|
}
|
|
driver.startUpdatingLocation()
|
|
}
|
|
|
|
func stopUpdating() {
|
|
driver.stopUpdatingLocation()
|
|
}
|
|
|
|
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
MainActor.assumeIsolated {
|
|
authorizationStatus = driver.authorizationStatus
|
|
RuntimeLogger.info("APP", "实时定位", "定位授权状态变化", details: [
|
|
"授权状态": authorizationName(authorizationStatus),
|
|
"requestID": activeRequest.map { String($0.id) } ?? "nil",
|
|
"阶段": activeRequest.map { phaseName($0.phase) } ?? "idle"
|
|
])
|
|
switch authorizationStatus {
|
|
case .authorizedAlways, .authorizedWhenInUse:
|
|
if let request = activeRequest, request.phase == .awaitingAuthorization {
|
|
beginOneShot(for: request.id)
|
|
}
|
|
case .denied, .restricted:
|
|
finishRequest(id: activeRequest?.id, coordinate: nil)
|
|
case .notDetermined:
|
|
break
|
|
@unknown default:
|
|
finishRequest(id: activeRequest?.id, coordinate: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
MainActor.assumeIsolated {
|
|
RuntimeLogger.info("APP", "实时定位", "CLLocationManager 返回样本批次", details: [
|
|
"样本数": String(locations.count),
|
|
"requestID": activeRequest.map { String($0.id) } ?? "nil",
|
|
"阶段": activeRequest.map { phaseName($0.phase) } ?? "idle"
|
|
])
|
|
for (index, candidate) in locations.enumerated() {
|
|
let valid = Self.isValid(candidate)
|
|
let freshEnough = activeRequest?.startedAt.map {
|
|
candidate.timestamp >= $0.addingTimeInterval(-1)
|
|
} ?? false
|
|
RealtimeLocationTrace.log(
|
|
valid ? "检查 CLLocationManager 样本" : "拒绝 CLLocationManager 样本:坐标或精度无效",
|
|
location: candidate,
|
|
details: [
|
|
"批次索引": String(index),
|
|
"基础校验通过": String(valid),
|
|
"满足当前请求时间窗": String(freshEnough)
|
|
],
|
|
level: valid ? .info : .warning
|
|
)
|
|
}
|
|
let validLocations = locations.filter(Self.isValid)
|
|
if let latestValid = validLocations.last {
|
|
location = latestValid
|
|
}
|
|
|
|
guard let request = activeRequest,
|
|
let startedAt = request.startedAt,
|
|
request.phase != .awaitingAuthorization,
|
|
let latest = validLocations.last(where: {
|
|
$0.timestamp >= startedAt.addingTimeInterval(-1)
|
|
}) else {
|
|
if let request = activeRequest, request.phase != .awaitingAuthorization {
|
|
RuntimeLogger.warning("APP", "实时定位", "本批次没有可完成当前请求的样本", details: [
|
|
"requestID": String(request.id),
|
|
"阶段": phaseName(request.phase),
|
|
"有效样本数": String(validLocations.count)
|
|
])
|
|
}
|
|
return
|
|
}
|
|
RealtimeLocationTrace.log("接受 CLLocationManager 样本并完成请求", location: latest, details: [
|
|
"requestID": String(request.id),
|
|
"阶段": phaseName(request.phase)
|
|
])
|
|
finishRequest(id: request.id, coordinate: latest.coordinate)
|
|
}
|
|
}
|
|
|
|
nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
MainActor.assumeIsolated {
|
|
guard let request = activeRequest else { return }
|
|
RuntimeLogger.warning("APP", "定位", "定位回调失败", details: [
|
|
"requestID": String(request.id),
|
|
"error": error.localizedDescription
|
|
])
|
|
|
|
let nsError = error as NSError
|
|
let isAuthorizationDenied = nsError.domain == kCLErrorDomain
|
|
&& nsError.code == CLError.denied.rawValue
|
|
|
|
if request.phase == .oneShot,
|
|
!isAuthorizationDenied,
|
|
authorizationStatus != .denied,
|
|
authorizationStatus != .restricted {
|
|
beginFallback(for: request.id)
|
|
} else {
|
|
finishRequest(id: request.id, coordinate: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func freshestCachedLocation(now: Date = Date()) -> CLLocation? {
|
|
let candidates = [location, driver.location].compactMap { $0 }
|
|
for candidate in candidates {
|
|
let valid = Self.isValid(candidate)
|
|
let age = abs(candidate.timestamp.timeIntervalSince(now))
|
|
if !valid || age > cacheMaxAge {
|
|
RealtimeLocationTrace.log("跳过 CLLocationManager 缓存样本", location: candidate, details: [
|
|
"基础校验通过": String(valid),
|
|
"缓存时效通过": String(age <= cacheMaxAge),
|
|
"缓存上限秒": String(Int(cacheMaxAge))
|
|
], level: .warning)
|
|
}
|
|
}
|
|
return candidates
|
|
.filter(Self.isValid)
|
|
.filter { abs($0.timestamp.timeIntervalSince(now)) <= cacheMaxAge }
|
|
.max(by: { $0.timestamp < $1.timestamp })
|
|
}
|
|
|
|
private static func isValid(_ location: CLLocation) -> Bool {
|
|
CLLocationCoordinate2DIsValid(location.coordinate)
|
|
&& location.horizontalAccuracy >= 0
|
|
}
|
|
|
|
private func scheduleTimeout(for requestID: UInt64, nanoseconds: UInt64) {
|
|
timeoutTask?.cancel()
|
|
timeoutTask = Task { [weak self] in
|
|
do {
|
|
try await Task.sleep(nanoseconds: nanoseconds)
|
|
} catch {
|
|
return
|
|
}
|
|
guard !Task.isCancelled else { return }
|
|
self?.handleTimeout(for: requestID)
|
|
}
|
|
}
|
|
|
|
private func handleTimeout(for requestID: UInt64) {
|
|
guard let request = activeRequest, request.id == requestID else { return }
|
|
switch request.phase {
|
|
case .awaitingAuthorization:
|
|
RuntimeLogger.warning("APP", "定位", "等待定位授权超时", details: ["requestID": String(requestID)])
|
|
finishRequest(id: requestID, coordinate: nil)
|
|
case .oneShot:
|
|
RuntimeLogger.warning("APP", "定位", "单次定位超时,切换持续定位", details: ["requestID": String(requestID)])
|
|
beginFallback(for: requestID)
|
|
case .continuousFallback:
|
|
RuntimeLogger.warning("APP", "定位", "持续定位超时", details: ["requestID": String(requestID)])
|
|
finishRequest(id: requestID, coordinate: nil)
|
|
}
|
|
}
|
|
|
|
private func beginOneShot(for requestID: UInt64) {
|
|
guard var request = activeRequest,
|
|
request.id == requestID,
|
|
request.phase == .awaitingAuthorization else { return }
|
|
request.phase = .oneShot
|
|
request.startedAt = Date()
|
|
activeRequest = request
|
|
RuntimeLogger.info("APP", "实时定位", "开始 CLLocationManager 单次定位", details: [
|
|
"requestID": String(requestID),
|
|
"超时毫秒": String(oneShotTimeoutNanoseconds / 1_000_000)
|
|
])
|
|
scheduleTimeout(for: requestID, nanoseconds: oneShotTimeoutNanoseconds)
|
|
driver.requestLocation()
|
|
}
|
|
|
|
private func beginFallback(for requestID: UInt64) {
|
|
guard var request = activeRequest,
|
|
request.id == requestID,
|
|
request.phase == .oneShot else { return }
|
|
request.phase = .continuousFallback
|
|
activeRequest = request
|
|
RuntimeLogger.info("APP", "实时定位", "开始 CLLocationManager 持续定位兜底", details: [
|
|
"requestID": String(requestID),
|
|
"超时毫秒": String(fallbackTimeoutNanoseconds / 1_000_000)
|
|
])
|
|
driver.startUpdatingLocation()
|
|
scheduleTimeout(for: requestID, nanoseconds: fallbackTimeoutNanoseconds)
|
|
}
|
|
|
|
private func finishRequest(id requestID: UInt64?, coordinate: CLLocationCoordinate2D?) {
|
|
guard let requestID,
|
|
let request = activeRequest,
|
|
request.id == requestID else { return }
|
|
|
|
timeoutTask?.cancel()
|
|
timeoutTask = nil
|
|
if request.phase == .continuousFallback {
|
|
driver.stopUpdatingLocation()
|
|
}
|
|
activeRequest = nil
|
|
isRequesting = false
|
|
|
|
if coordinate != nil {
|
|
RuntimeLogger.info("APP", "实时定位", "CLLocationManager 请求完成", details: [
|
|
"requestID": String(requestID),
|
|
"最终阶段": phaseName(request.phase),
|
|
"有坐标": "true"
|
|
])
|
|
} else {
|
|
RuntimeLogger.warning("APP", "实时定位", "CLLocationManager 请求结束但没有坐标", details: [
|
|
"requestID": String(requestID),
|
|
"最终阶段": phaseName(request.phase),
|
|
"有坐标": "false"
|
|
])
|
|
}
|
|
request.continuation.resume(returning: coordinate)
|
|
}
|
|
|
|
private func phaseName(_ phase: RequestPhase) -> String {
|
|
switch phase {
|
|
case .awaitingAuthorization: return "awaitingAuthorization"
|
|
case .oneShot: return "oneShot"
|
|
case .continuousFallback: return "continuousFallback"
|
|
}
|
|
}
|
|
|
|
private func authorizationName(_ status: CLAuthorizationStatus) -> String {
|
|
switch status {
|
|
case .notDetermined: return "notDetermined"
|
|
case .restricted: return "restricted"
|
|
case .denied: return "denied"
|
|
case .authorizedAlways: return "authorizedAlways"
|
|
case .authorizedWhenInUse: return "authorizedWhenInUse"
|
|
@unknown default: return "unknown(\(status.rawValue))"
|
|
}
|
|
}
|
|
}
|