release: PaopaoLocationSpoofer v1.0.0

- iOS 虚拟定位工具,基于本地 HTTP 代理 MITM 方案
- MapKit 原生地图体验,支持搜索、收藏、实时定位
- 完整的设置引导流程(证书安装、WiFi 代理配置、环境验证)
- 支持 iOS 15+,SwiftUI 构建
This commit is contained in:
xweiba
2026-08-05 15:39:57 +08:00
commit e137ff10e9
75 changed files with 7339 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
import Foundation
enum AppGroup {
static let identifier = "group.com.paopaolabs.location-spoofer"
static let defaults = UserDefaults(suiteName: identifier) ?? .standard
static var sharedContainerURL: URL? {
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier)
}
static var isSharedContainerAvailable: Bool { sharedContainerURL != nil }
static var containerURL: URL {
if let url = sharedContainerURL { return url }
return FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("LocationSpoofer", isDirectory: true)
}
}
enum WlocKeys {
static let coords = "wloc_settings"
}
struct WlocSettings: Codable {
var longitude: Double
var latitude: Double
var accuracy: Int
var enabled: Bool
}
enum WlocSettingsStore {
static func load() -> WlocSettings? {
guard let data = AppGroup.defaults.data(forKey: WlocKeys.coords) else { return nil }
return try? JSONDecoder().decode(WlocSettings.self, from: data)
}
static func save(_ settings: WlocSettings) {
guard let data = try? JSONEncoder().encode(settings) else { return }
AppGroup.defaults.set(data, forKey: WlocKeys.coords)
}
static func clear() {
save(WlocSettings(longitude: 0, latitude: 0, accuracy: 25, enabled: false))
}
}
+62
View File
@@ -0,0 +1,62 @@
import AVFoundation
import UIKit
final class BackgroundKeepAlive {
static let shared = BackgroundKeepAlive()
private var engine: AVAudioEngine?
private var playerNode: AVAudioPlayerNode?
private var isActive = false
private init() {
NotificationCenter.default.addObserver(
self, selector: #selector(handleInterruption),
name: AVAudioSession.interruptionNotification, object: nil)
}
@objc private func handleInterruption(_ notification: Notification) {
guard isActive, let info = notification.userInfo,
let type = info[AVAudioSessionInterruptionTypeKey] as? UInt,
type == AVAudioSession.InterruptionType.ended.rawValue else { return }
start()
RuntimeLogger.info("APP", "KeepAlive", "音频中断恢复")
}
func start() {
guard !isActive else { return }
isActive = true
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: .mixWithOthers)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
RuntimeLogger.error("APP", "KeepAlive", "音频会话失败", error: error)
}
let eng = AVAudioEngine()
let player = AVAudioPlayerNode()
eng.attach(player)
let fmt = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 1)!
let buf = AVAudioPCMBuffer(pcmFormat: fmt, frameCapacity: 44100 * 3)!
buf.frameLength = 44100 * 3
eng.connect(player, to: eng.mainMixerNode, format: fmt)
eng.prepare()
do { try eng.start() } catch {
RuntimeLogger.error("APP", "KeepAlive", "引擎启动失败", error: error)
isActive = false; return
}
player.scheduleBuffer(buf, at: nil, options: .loops)
player.play()
engine = eng; playerNode = player
UIApplication.shared.isIdleTimerDisabled = true
RuntimeLogger.info("APP", "KeepAlive", "后台保活已启动(静音音频)")
}
func stop() {
guard isActive else { return }
isActive = false
playerNode?.stop(); engine?.stop()
playerNode = nil; engine = nil
UIApplication.shared.isIdleTimerDisabled = false
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
RuntimeLogger.info("APP", "KeepAlive", "后台保活已停止")
}
}
+46
View File
@@ -0,0 +1,46 @@
import Foundation
final class CertificateAuthorityStore {
private let directory: URL
private let generator: () throws -> CertificateAuthority
private let certificateURL: URL
private let keyURL: URL
init(directory: URL = AppGroup.containerURL.appendingPathComponent("CertificateAuthority", isDirectory: true), generator: @escaping () throws -> CertificateAuthority = CoreBridge.generateCertificateAuthority) {
self.directory = directory
self.generator = generator
self.certificateURL = directory.appendingPathComponent("ca-cert.pem")
self.keyURL = directory.appendingPathComponent("ca-key.pem")
}
func ensure() throws -> CertificateAuthority {
if let current = try load() {
RuntimeLogger.debug("SHARED", "Certificate.store", "读取已有 CA 文件", details: ["directory": directory.path])
return current
}
RuntimeLogger.info("SHARED", "Certificate.store", "未找到 CA 文件,开始生成", details: ["directory": directory.path])
let authority = try generator()
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
guard let certificateData = authority.certPEM.data(using: .utf8),
let keyData = authority.keyPEM.data(using: .utf8) else {
throw CocoaError(.fileWriteInapplicableStringEncoding)
}
try certificateData.write(to: certificateURL, options: .atomic)
try keyData.write(to: keyURL, options: .atomic)
applyCompleteProtection(to: certificateURL)
applyCompleteProtection(to: keyURL)
RuntimeLogger.info("SHARED", "Certificate.store", "CA 文件写入完成", details: ["directory": directory.path])
return authority
}
func load() throws -> CertificateAuthority? {
guard FileManager.default.fileExists(atPath: certificateURL.path), FileManager.default.fileExists(atPath: keyURL.path) else { return nil }
return CertificateAuthority(certPEM: try String(contentsOf: certificateURL, encoding: .utf8), keyPEM: try String(contentsOf: keyURL, encoding: .utf8))
}
private func applyCompleteProtection(to url: URL) {
#if os(iOS)
try? FileManager.default.setAttributes([.protectionKey: FileProtectionType.complete], ofItemAtPath: url.path)
#endif
}
}
+41
View File
@@ -0,0 +1,41 @@
import Foundation
enum CertificateTrustState: Equatable {
case checking
case trusted
case unavailable
var canModify: Bool { self == .trusted }
var message: String {
switch self {
case .checking: return "checking..."
case .trusted: return "trusted"
case .unavailable: return "not configured"
}
}
}
enum LocationActionState: Equatable {
case idle
case applyingLocation
case failed(String)
var isBusy: Bool {
if case .applyingLocation = self { return true }
return false
}
var isFailure: Bool {
if case .failed = self { return true }
return false
}
var statusTitle: String {
switch self {
case .idle: return ""
case .applyingLocation: return "applying..."
case .failed: return "failed"
}
}
}
+40
View File
@@ -0,0 +1,40 @@
import Foundation
import Security
final class CertificateTrustVerifier {
/// Check whether a CA certificate (given as PEM data) is installed and fully trusted
/// by the system. Uses SecTrust evaluation against system anchors only no network needed.
static func isCACertificateTrusted(certPEM: String) -> Bool {
guard let certData = certPEM.data(using: .utf8),
let cert = SecCertificateCreateWithData(nil, certData as CFData) else {
RuntimeLogger.error("APP", "Trust", "无法解析 CA 证书 PEM")
return false
}
// Create a basic trust with the CA cert, using system anchor certificates only
var trust: SecTrust?
let createStatus = SecTrustCreateWithCertificates(
[cert] as CFArray,
SecPolicyCreateBasicX509(),
&trust
)
guard createStatus == errSecSuccess, let trust = trust else {
RuntimeLogger.error("APP", "Trust", "无法创建 SecTrust")
return false
}
// Use system anchors only if our CA is installed & trusted, evaluation passes
SecTrustSetAnchorCertificatesOnly(trust, false)
var error: CFError?
let result = SecTrustEvaluateWithError(trust, &error)
if let error {
RuntimeLogger.warning("APP", "Trust", "SecTrust 评估返回错误", details: [
"error": (error as Error).localizedDescription
])
}
RuntimeLogger.info("APP", "Trust", result ? "CA 证书已被系统信任" : "CA 证书未被系统信任")
return result
}
}
+62
View File
@@ -0,0 +1,62 @@
import Foundation
/// GCJ-02 () WGS-84
///
/// MKMapView 使 (AutoNavi) GCJ-02
/// MKMapView `centerCoordinate``convert(point:toCoordinateFrom:)`
/// GCJ-02 CoreLocation / CLLocationManager
/// Apple wloc 使 WGS-84
///
///
/// - UI GCJ-02
/// - wloc WGS-84
///
/// UI GCJ-02 WGS-84
enum CoordinateConverter {
// (Krasovsky 1940)
private static let a = 6378245.0
private static let ee = 0.00669342162296594323
/// GCJ-02 WGS-84 0.5
static func gcj02ToWgs84(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
var wgsLat = lat
var wgsLon = lon
//
for _ in 0..<2 {
let d = delta(lat: wgsLat, lon: wgsLon)
wgsLat = lat - d.lat
wgsLon = lon - d.lon
}
return (wgsLat, wgsLon)
}
/// (WGS-84 GCJ-02 )
private static func delta(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
let dLat = transformLat(x: lon - 105.0, y: lat - 35.0)
let dLon = transformLon(x: lon - 105.0, y: lat - 35.0)
let radLat = lat / 180.0 * .pi
var magic = sin(radLat)
magic = 1 - ee * magic * magic
let sqrtMagic = sqrt(magic)
return (
lat: (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * .pi),
lon: (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * .pi)
)
}
private static func transformLat(x: Double, y: Double) -> Double {
var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(abs(x))
ret += (20.0 * sin(6.0 * x * .pi) + 20.0 * sin(2.0 * x * .pi)) * 2.0 / 3.0
ret += (20.0 * sin(y * .pi) + 40.0 * sin(y / 3.0 * .pi)) * 2.0 / 3.0
ret += (160.0 * sin(y / 12.0 * .pi) + 320.0 * sin(y * .pi / 30.0)) * 2.0 / 3.0
return ret
}
private static func transformLon(x: Double, y: Double) -> Double {
var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(abs(x))
ret += (20.0 * sin(6.0 * x * .pi) + 20.0 * sin(2.0 * x * .pi)) * 2.0 / 3.0
ret += (20.0 * sin(x * .pi) + 40.0 * sin(x / 3.0 * .pi)) * 2.0 / 3.0
ret += (150.0 * sin(x / 12.0 * .pi) + 300.0 * sin(x / 30.0 * .pi)) * 2.0 / 3.0
return ret
}
}
+126
View File
@@ -0,0 +1,126 @@
import Foundation
import Darwin
struct CertificateAuthority: Equatable {
let certPEM: String
let keyPEM: String
}
enum CoreBridgeError: LocalizedError {
case generationFailed
case serverStartFailed
var errorDescription: String? {
switch self {
case .generationFailed: return "无法生成本地证书"
case .serverStartFailed: return "无法启动本地证书服务"
}
}
}
enum CoreBridge {
static func generateCertificateAuthority() throws -> CertificateAuthority {
RuntimeLogger.info("APP", "Core.CA", "调用 Go Core 生成 CA")
let result = wloccore_generateca()
guard let certPointer = result.r0, let keyPointer = result.r1 else {
flushLogs(category: "CA")
throw CoreBridgeError.generationFailed
}
defer { free(certPointer); free(keyPointer) }
RuntimeLogger.info("APP", "Core.CA", "Go Core CA 生成成功")
flushLogs(category: "CA")
return CertificateAuthority(certPEM: String(cString: certPointer), keyPEM: String(cString: keyPointer))
}
/// Go Core wloc
static func testWlocPatch(lat: Double, lon: Double, accuracy: Int) -> String {
guard let ptr = wloccore_testpatch(CDouble(lat), CDouble(lon), CInt(accuracy)) else {
return "error: null result"
}
defer { free(ptr) }
return String(cString: ptr)
}
/// wloc WiFi AP +
static func testWlocRequestData() -> Data {
guard let ptr = wloccore_testrequesthex() else { return Data([0x0a, 0x02, 0x08, 0x01]) }
defer { free(ptr) }
let hex = String(cString: ptr)
var data = Data()
var idx = hex.startIndex
while idx < hex.endIndex {
let end = hex.index(idx, offsetBy: 2, limitedBy: hex.endIndex) ?? hex.endIndex
if let b = UInt8(hex[idx..<end], radix: 16) { data.append(b) }
idx = end
}
return data
}
static func flushLogs(category: String) {
guard let pointer = wloccore_drainlogs() else { return }
defer { free(pointer) }
String(cString: pointer).split(separator: "\n").forEach {
RuntimeLogger.info("CORE", category, String($0))
}
}
static func refreshVerifyToken() -> String {
guard let ptr = wloccore_refreshverifytoken() else { return "" }
defer { free(ptr) }
return String(cString: ptr)
}
}
final class LocalCertificateServer {
private var handle: UInt = 0
private(set) var downloadURL: URL?
private(set) var probeURL: URL?
private(set) var leafHash = ""
deinit { stop() }
func start(authority: CertificateAuthority) throws {
if handle != 0 {
RuntimeLogger.debug("APP", "Certificate.server", "本地证书服务已在运行")
return
}
RuntimeLogger.info("APP", "Certificate.server", "调用 Go Core 启动本地证书服务")
let newHandle: UInt = authority.certPEM.withCString { certPointer in
authority.keyPEM.withCString { keyPointer in
UInt(wloccore_startcertserver(UnsafeMutablePointer(mutating: certPointer), UnsafeMutablePointer(mutating: keyPointer)))
}
}
guard newHandle != 0 else {
CoreBridge.flushLogs(category: "CertificateServer")
throw CoreBridgeError.serverStartFailed
}
let httpPort = Int(wloccore_certserver_httpport(newHandle))
let httpsPort = Int(wloccore_certserver_httpsport(newHandle))
guard httpPort > 0, httpsPort > 0, let hashPointer = wloccore_certserver_leafsha256(newHandle) else {
_ = wloccore_stopcertserver(newHandle)
throw CoreBridgeError.serverStartFailed
}
defer { free(hashPointer) }
handle = newHandle
downloadURL = URL(string: "http://127.0.0.1:\(httpPort)/ca.cer")
probeURL = URL(string: "https://127.0.0.1:\(httpsPort)/health")
leafHash = String(cString: hashPointer)
RuntimeLogger.info("APP", "Certificate.server", "本地证书服务启动成功", details: [
"httpPort": String(httpPort),
"httpsPort": String(httpsPort),
"leafHash": leafHash
])
CoreBridge.flushLogs(category: "CertificateServer")
}
func stop() {
guard handle != 0 else { return }
let result = wloccore_stopcertserver(handle)
RuntimeLogger.info("APP", "Certificate.server", "停止本地证书服务", details: ["result": String(result)])
CoreBridge.flushLogs(category: "CertificateServer")
handle = 0
downloadURL = nil
probeURL = nil
leafHash = ""
}
}
+90
View File
@@ -0,0 +1,90 @@
import Foundation
struct FavoriteLocation: Codable, Identifiable, Equatable {
let id: UUID
var name: String
var latitude: Double
var longitude: Double
var accuracy: Int
var createdAt: Date
init(id: UUID = UUID(), name: String, latitude: Double, longitude: Double, accuracy: Int, createdAt: Date = Date()) {
self.id = id
self.name = name
self.latitude = latitude
self.longitude = longitude
self.accuracy = accuracy
self.createdAt = createdAt
}
}
struct MapConfiguration: Equatable {
let showsUserLocation: Bool
let allowsCurrentLocationRequest: Bool
static let `default` = MapConfiguration(showsUserLocation: false, allowsCurrentLocationRequest: false)
}
final class FavoriteLocationStore: ObservableObject {
private enum Keys {
static let favorites = "favorite_locations"
static let selectedID = "favorite_locations_selected_id"
}
@Published private(set) var favorites: [FavoriteLocation]
@Published private(set) var selectedFavoriteID: UUID?
private let defaults: UserDefaults
init(defaults: UserDefaults = AppGroup.defaults) {
self.defaults = defaults
if let data = defaults.data(forKey: Keys.favorites),
let decoded = try? JSONDecoder().decode([FavoriteLocation].self, from: data) {
self.favorites = decoded
} else {
self.favorites = []
}
self.selectedFavoriteID = defaults.string(forKey: Keys.selectedID).flatMap(UUID.init(uuidString:))
}
var selectedFavorite: FavoriteLocation? {
guard let selectedFavoriteID else { return nil }
return favorites.first(where: { $0.id == selectedFavoriteID })
}
@discardableResult
func save(name: String, latitude: Double, longitude: Double, accuracy: Int) -> FavoriteLocation {
let favorite = FavoriteLocation(name: name, latitude: latitude, longitude: longitude, accuracy: accuracy)
//
favorites.removeAll {
abs($0.latitude - favorite.latitude) < 0.000001 && abs($0.longitude - favorite.longitude) < 0.000001
}
favorites.insert(favorite, at: 0)
select(favorite.id)
persist()
return favorite
}
func select(_ id: UUID?) {
selectedFavoriteID = id
defaults.set(id?.uuidString, forKey: Keys.selectedID)
}
func rename(_ id: UUID, to name: String) {
guard let idx = favorites.firstIndex(where: { $0.id == id }) else { return }
favorites[idx].name = name
persist()
}
func delete(_ favorite: FavoriteLocation) {
favorites.removeAll { $0.id == favorite.id }
if selectedFavoriteID == favorite.id {
select(favorites.first?.id)
}
persist()
}
private func persist() {
guard let data = try? JSONEncoder().encode(favorites) else { return }
defaults.set(data, forKey: Keys.favorites)
}
}
+26
View File
@@ -0,0 +1,26 @@
import Network
import Foundation
@MainActor
final class NetworkMonitor: ObservableObject {
static let shared = NetworkMonitor()
@Published private(set) var isSatisfied = true
@Published private(set) var isWiFiEnabled = true
private let monitor = NWPathMonitor()
private init() {
monitor.pathUpdateHandler = { [weak self] path in
let satisfied = path.status == .satisfied
let wifi = path.usesInterfaceType(.wifi)
Task { @MainActor in
self?.isSatisfied = satisfied
self?.isWiFiEnabled = wifi
}
}
monitor.start(queue: .main)
}
var isAirplaneMode: Bool { !isSatisfied }
}
+164
View File
@@ -0,0 +1,164 @@
import Foundation
struct RuntimeLogEntry: Codable, Identifiable, Equatable {
enum Level: String, Codable {
case debug
case info
case warning
case error
}
let id: UUID
let timestamp: Date
let source: String
let level: Level
let category: String
let message: String
let details: [String: String]
init(
id: UUID = UUID(),
timestamp: Date = Date(),
source: String,
level: Level,
category: String,
message: String,
details: [String: String] = [:]
) {
self.id = id
self.timestamp = timestamp
self.source = source
self.level = level
self.category = category
self.message = message
self.details = details
}
var renderedText: String {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let suffix = details.isEmpty
? ""
: " " + details.sorted(by: { $0.key < $1.key }).map { "\($0.key)=\($0.value)" }.joined(separator: " ")
return "\(formatter.string(from: timestamp)) [\(source)] [\(level.rawValue.uppercased())] [\(category)] \(message)\(suffix)"
}
}
enum RuntimeLogStore {
private static let lock = NSLock()
private static let decoder = JSONDecoder()
private static let encoder = JSONEncoder()
private static let maximumBytes: UInt64 = 1_500_000
static func append(_ entry: RuntimeLogEntry) {
lock.lock()
defer { lock.unlock() }
do {
let url = try logURL(for: entry.source)
try rotateIfNeeded(url)
var data = try encoder.encode(entry)
data.append(0x0A)
if !FileManager.default.fileExists(atPath: url.path) {
try data.write(to: url, options: .atomic)
return
}
let handle = try FileHandle(forWritingTo: url)
defer { try? handle.close() }
handle.seekToEndOfFile()
handle.write(data)
handle.synchronizeFile()
} catch {
NSLog("RuntimeLogStore append failed: %@", error.localizedDescription)
}
}
static func loadAll(limit: Int = 800) -> [RuntimeLogEntry] {
lock.lock()
defer { lock.unlock() }
let directory = AppGroup.containerURL.appendingPathComponent("RuntimeLogs", isDirectory: true)
guard let urls = try? FileManager.default.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: nil
) else { return [] }
let entries = urls
.filter { $0.pathExtension == "jsonl" }
.flatMap(readEntries)
.sorted { $0.timestamp < $1.timestamp }
return Array(entries.suffix(limit))
}
static func clearAll() {
lock.lock()
defer { lock.unlock() }
let directory = AppGroup.containerURL.appendingPathComponent("RuntimeLogs", isDirectory: true)
try? FileManager.default.removeItem(at: directory)
}
private static func logURL(for source: String) throws -> URL {
let directory = AppGroup.containerURL.appendingPathComponent("RuntimeLogs", isDirectory: true)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let process = (Bundle.main.bundleIdentifier ?? "unknown-process")
.replacingOccurrences(of: "/", with: "-")
let safeSource = source.replacingOccurrences(of: "/", with: "-")
return directory.appendingPathComponent("\(process)-\(safeSource).jsonl")
}
private static func rotateIfNeeded(_ url: URL) throws {
guard let attributes = try? FileManager.default.attributesOfItem(atPath: url.path),
let size = attributes[.size] as? NSNumber,
size.uint64Value >= maximumBytes else { return }
let backup = url.deletingPathExtension().appendingPathExtension("previous.jsonl")
try? FileManager.default.removeItem(at: backup)
try FileManager.default.moveItem(at: url, to: backup)
}
private static func readEntries(_ url: URL) -> [RuntimeLogEntry] {
guard let data = try? Data(contentsOf: url),
let text = String(data: data, encoding: .utf8) else { return [] }
return text.split(separator: "\n").compactMap { line in
guard let data = String(line).data(using: .utf8) else { return nil }
return try? decoder.decode(RuntimeLogEntry.self, from: data)
}
}
}
enum RuntimeLogger {
static func debug(_ source: String, _ category: String, _ message: String, details: [String: String] = [:]) {
write(.debug, source: source, category: category, message: message, details: details)
}
static func info(_ source: String, _ category: String, _ message: String, details: [String: String] = [:]) {
write(.info, source: source, category: category, message: message, details: details)
}
static func warning(_ source: String, _ category: String, _ message: String, details: [String: String] = [:]) {
write(.warning, source: source, category: category, message: message, details: details)
}
static func error(_ source: String, _ category: String, _ message: String, error: Error? = nil, details: [String: String] = [:]) {
var values = details
if let error {
let nsError = error as NSError
values["error.domain"] = nsError.domain
values["error.code"] = String(nsError.code)
values["error.description"] = nsError.localizedDescription
if !nsError.userInfo.isEmpty {
values["error.userInfo"] = nsError.userInfo
.map { "\($0.key)=\(String(describing: $0.value))" }
.sorted()
.joined(separator: "; ")
}
}
write(.error, source: source, category: category, message: message, details: values)
}
private static func write(_ level: RuntimeLogEntry.Level, source: String, category: String, message: String, details: [String: String]) {
RuntimeLogStore.append(RuntimeLogEntry(
source: source,
level: level,
category: category,
message: message,
details: details
))
}
}
+39
View File
@@ -0,0 +1,39 @@
import Foundation
/// UI
enum VerificationResult: Equatable, Identifiable {
case success
case proxyNotRunning
case verificationInProgress
case verificationSuperseded
case certNotTrusted
case wifiProxyNotConfigured
case coordinateWriteFailed(String)
case patchFailed(String)
var id: String {
switch self {
case .success: return "成功"
case .proxyNotRunning: return "代理未运行"
case .verificationInProgress: return "已有验证正在进行"
case .verificationSuperseded: return "验证已被新位置取代"
case .certNotTrusted: return "证书未信任"
case .wifiProxyNotConfigured: return "WiFi代理未配置"
case .coordinateWriteFailed: return "坐标写入失败"
case .patchFailed: return "改写验证失败"
}
}
var isSuccess: Bool { self == .success }
///
var tipKind: TipKind? {
switch self {
case .success: return nil
case .proxyNotRunning, .verificationInProgress, .verificationSuperseded: return nil
case .certNotTrusted: return nil // tip
case .wifiProxyNotConfigured: return .proxySetup
case .coordinateWriteFailed, .patchFailed: return .rewriteFailed
}
}
}