mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 04:59:05 +08:00
Merge pull request #51 from SongXiaoXi/main
feat: add host location passthrough to guest VM
This commit is contained in:
16
Makefile
16
Makefile
@@ -12,6 +12,9 @@ CFW_INPUT ?= cfw_input
|
||||
# ─── Paths ────────────────────────────────────────────────────────
|
||||
SCRIPTS := scripts
|
||||
BINARY := .build/release/vphone-cli
|
||||
BUNDLE := .build/vphone-cli.app
|
||||
BUNDLE_BIN := $(BUNDLE)/Contents/MacOS/vphone-cli
|
||||
INFO_PLIST := sources/Info.plist
|
||||
ENTITLEMENTS := sources/vphone.entitlements
|
||||
VENV := .venv
|
||||
LIMD_PREFIX := .limd
|
||||
@@ -83,7 +86,7 @@ setup_libimobiledevice:
|
||||
# Build
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
.PHONY: build install clean
|
||||
.PHONY: build install clean bundle
|
||||
|
||||
build: $(BINARY)
|
||||
|
||||
@@ -95,6 +98,13 @@ $(BINARY): $(SWIFT_SOURCES) Package.swift $(ENTITLEMENTS)
|
||||
codesign --force --sign - --entitlements $(ENTITLEMENTS) $@
|
||||
@echo " signed OK"
|
||||
|
||||
bundle: build $(INFO_PLIST)
|
||||
@mkdir -p $(BUNDLE)/Contents/MacOS
|
||||
@cp -f $(BINARY) $(BUNDLE_BIN)
|
||||
@cp -f $(INFO_PLIST) $(BUNDLE)/Contents/Info.plist
|
||||
@codesign --force --sign - --entitlements $(ENTITLEMENTS) $(BUNDLE_BIN)
|
||||
@echo " bundled → $(BUNDLE)"
|
||||
|
||||
install: build
|
||||
mkdir -p ./bin
|
||||
cp -f $(BINARY) ./bin/vphone-cli
|
||||
@@ -137,8 +147,8 @@ vphoned_sign: $(SCRIPTS)/vphoned/vphoned
|
||||
vm_new:
|
||||
zsh $(SCRIPTS)/vm_create.sh --dir $(VM_DIR) --disk-size $(DISK_SIZE)
|
||||
|
||||
boot: build vphoned_sign
|
||||
cd $(VM_DIR) && "$(CURDIR)/$(BINARY)" \
|
||||
boot: bundle vphoned_sign
|
||||
cd $(VM_DIR) && "$(CURDIR)/$(BUNDLE_BIN)" \
|
||||
--rom ./AVPBooter.vresearch1.bin \
|
||||
--disk ./Disk.img \
|
||||
--nvram ./nvram.bin \
|
||||
|
||||
@@ -23,6 +23,7 @@ let package = Package(
|
||||
.linkedFramework("Virtualization"),
|
||||
.linkedFramework("AppKit"),
|
||||
.linkedFramework("SwiftUI"),
|
||||
.linkedFramework("CoreLocation"),
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
@@ -232,7 +232,7 @@ wait_for_recovery() {
|
||||
start_iproxy_2222() {
|
||||
local iproxy_bin
|
||||
iproxy_bin="${PROJECT_ROOT}/.limd/bin/iproxy"
|
||||
[[ -n "$iproxy_bin" ]] || die "iproxy not found in PATH"
|
||||
[[ -x "$iproxy_bin" ]] || die "iproxy not found at $iproxy_bin (run: make setup_libimobiledevice)"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
: > "$IPROXY_LOG"
|
||||
@@ -262,6 +262,10 @@ stop_iproxy_2222() {
|
||||
}
|
||||
|
||||
main() {
|
||||
check_platform
|
||||
install_brew_deps
|
||||
ensure_python_linked
|
||||
|
||||
run_make "Project setup" setup_libimobiledevice
|
||||
run_make "Project setup" setup_venv
|
||||
run_make "Project setup" build
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<string>data-allowed</string>
|
||||
<string>data-allowed-write</string>
|
||||
</array>
|
||||
<key>com.apple.locationd.preauthorized</key>
|
||||
<true/>
|
||||
<key>com.apple.locationd.simulation</key>
|
||||
<true/>
|
||||
<key>com.apple.Pasteboard.background-access</key>
|
||||
<true/>
|
||||
<key>com.apple.Pasteboard.originating-bundle-id-query</key>
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
#include <dlfcn.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#include <objc/runtime.h>
|
||||
#include <objc/message.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
@@ -250,6 +252,169 @@ static BOOL devmode_arm(BOOL *alreadyEnabled) {
|
||||
return success && [success boolValue];
|
||||
}
|
||||
|
||||
// MARK: - CoreLocation Simulation
|
||||
|
||||
static id gSimManager = nil;
|
||||
static SEL gSetLocationSel = NULL;
|
||||
static SEL gClearLocationsSel = NULL;
|
||||
static SEL gFlushSel = NULL;
|
||||
static SEL gStartSimSel = NULL;
|
||||
static BOOL gLocationLoaded = NO;
|
||||
|
||||
static BOOL load_corelocation(void) {
|
||||
void *h = dlopen("/System/Library/Frameworks/CoreLocation.framework/CoreLocation", RTLD_NOW);
|
||||
if (!h) { NSLog(@"vphoned: dlopen CoreLocation failed"); return NO; }
|
||||
|
||||
Class cls = NSClassFromString(@"CLSimulationManager");
|
||||
if (!cls) { NSLog(@"vphoned: CLSimulationManager not found"); return NO; }
|
||||
|
||||
gSimManager = [[cls alloc] init];
|
||||
if (!gSimManager) { NSLog(@"vphoned: CLSimulationManager alloc/init failed"); return NO; }
|
||||
|
||||
// Probe available selectors for setting location
|
||||
SEL candidates[] = {
|
||||
NSSelectorFromString(@"setSimulatedLocation:"),
|
||||
NSSelectorFromString(@"appendSimulatedLocation:"),
|
||||
NSSelectorFromString(@"setLocation:"),
|
||||
};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if ([gSimManager respondsToSelector:candidates[i]]) {
|
||||
gSetLocationSel = candidates[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!gSetLocationSel) {
|
||||
NSLog(@"vphoned: no set-location selector found, dumping methods:");
|
||||
unsigned int count = 0;
|
||||
Method *methods = class_copyMethodList([gSimManager class], &count);
|
||||
for (unsigned int i = 0; i < count; i++) {
|
||||
NSLog(@" %s", sel_getName(method_getName(methods[i])));
|
||||
}
|
||||
free(methods);
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Probe clear selector
|
||||
SEL clearCandidates[] = {
|
||||
NSSelectorFromString(@"clearSimulatedLocations"),
|
||||
NSSelectorFromString(@"stopLocationSimulation"),
|
||||
};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if ([gSimManager respondsToSelector:clearCandidates[i]]) {
|
||||
gClearLocationsSel = clearCandidates[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Probe flush selector
|
||||
SEL flushCandidates[] = {
|
||||
NSSelectorFromString(@"flush"),
|
||||
NSSelectorFromString(@"flushSimulatedLocations"),
|
||||
};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if ([gSimManager respondsToSelector:flushCandidates[i]]) {
|
||||
gFlushSel = flushCandidates[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Probe startLocationSimulation selector
|
||||
SEL startCandidates[] = {
|
||||
NSSelectorFromString(@"startLocationSimulation"),
|
||||
NSSelectorFromString(@"startSimulation"),
|
||||
};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if ([gSimManager respondsToSelector:startCandidates[i]]) {
|
||||
gStartSimSel = startCandidates[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Start simulation session if available
|
||||
if (gStartSimSel) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[gSimManager performSelector:gStartSimSel];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
NSLog(@"vphoned: CoreLocation simulation loaded (set=%s, clear=%s, flush=%s, start=%s)",
|
||||
sel_getName(gSetLocationSel),
|
||||
gClearLocationsSel ? sel_getName(gClearLocationsSel) : "(none)",
|
||||
gFlushSel ? sel_getName(gFlushSel) : "(none)",
|
||||
gStartSimSel ? sel_getName(gStartSimSel) : "(none)");
|
||||
gLocationLoaded = YES;
|
||||
return YES;
|
||||
}
|
||||
|
||||
static void simulate_location(double lat, double lon, double alt,
|
||||
double hacc, double vacc,
|
||||
double speed, double course) {
|
||||
if (!gLocationLoaded || !gSimManager || !gSetLocationSel) return;
|
||||
|
||||
@try {
|
||||
// CLLocationCoordinate2D is {double latitude, double longitude}
|
||||
typedef struct { double latitude; double longitude; } CLCoord2D;
|
||||
CLCoord2D coord = {lat, lon};
|
||||
|
||||
// Use full CLLocation init including speed and course
|
||||
Class locClass = NSClassFromString(@"CLLocation");
|
||||
id locInst = [locClass alloc];
|
||||
SEL initSel = NSSelectorFromString(
|
||||
@"initWithCoordinate:altitude:horizontalAccuracy:verticalAccuracy:course:speed:timestamp:");
|
||||
if (![locInst respondsToSelector:initSel]) {
|
||||
// Fallback to simpler init
|
||||
initSel = NSSelectorFromString(
|
||||
@"initWithCoordinate:altitude:horizontalAccuracy:verticalAccuracy:timestamp:");
|
||||
typedef id (*InitFunc5)(id, SEL, CLCoord2D, double, double, double, id);
|
||||
id location = ((InitFunc5)objc_msgSend)(locInst, initSel, coord, alt, hacc, vacc, [NSDate date]);
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[gSimManager performSelector:gSetLocationSel withObject:location];
|
||||
if (gFlushSel) [gSimManager performSelector:gFlushSel];
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
NSLog(@"vphoned: simulate_location lat=%.6f lon=%.6f (fallback init) sel=%s%s",
|
||||
lat, lon, sel_getName(gSetLocationSel),
|
||||
gFlushSel ? " (flushed)" : "");
|
||||
return;
|
||||
}
|
||||
|
||||
typedef id (*InitFunc7)(id, SEL, CLCoord2D, double, double, double, double, double, id);
|
||||
id location = ((InitFunc7)objc_msgSend)(locInst, initSel, coord, alt, hacc, vacc, course, speed, [NSDate date]);
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[gSimManager performSelector:gSetLocationSel withObject:location];
|
||||
if (gFlushSel) {
|
||||
[gSimManager performSelector:gFlushSel];
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
NSLog(@"vphoned: simulate_location lat=%.6f lon=%.6f alt=%.1f spd=%.1f crs=%.1f sel=%s%s",
|
||||
lat, lon, alt, speed, course, sel_getName(gSetLocationSel),
|
||||
gFlushSel ? " (flushed)" : "");
|
||||
} @catch (NSException *e) {
|
||||
NSLog(@"vphoned: simulate_location exception: %@", e);
|
||||
}
|
||||
}
|
||||
|
||||
static void clear_simulated_location(void) {
|
||||
if (!gLocationLoaded || !gSimManager) return;
|
||||
@try {
|
||||
if (gClearLocationsSel) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[gSimManager performSelector:gClearLocationsSel];
|
||||
#pragma clang diagnostic pop
|
||||
NSLog(@"vphoned: cleared simulated location");
|
||||
}
|
||||
} @catch (NSException *e) {
|
||||
NSLog(@"vphoned: clear_simulated_location exception: %@", e);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Framing
|
||||
|
||||
static BOOL read_fully(int fd, void *buf, size_t count) {
|
||||
@@ -618,6 +783,23 @@ static NSDictionary *handle_command(NSDictionary *msg) {
|
||||
return make_response(@"pong", reqId);
|
||||
}
|
||||
|
||||
if ([type isEqualToString:@"location"]) {
|
||||
double lat = [msg[@"lat"] doubleValue];
|
||||
double lon = [msg[@"lon"] doubleValue];
|
||||
double alt = [msg[@"alt"] doubleValue];
|
||||
double hacc = [msg[@"hacc"] doubleValue];
|
||||
double vacc = [msg[@"vacc"] doubleValue];
|
||||
double speed = [msg[@"speed"] doubleValue];
|
||||
double course = [msg[@"course"] doubleValue];
|
||||
simulate_location(lat, lon, alt, hacc, vacc, speed, course);
|
||||
return make_response(@"ok", reqId);
|
||||
}
|
||||
|
||||
if ([type isEqualToString:@"location_stop"]) {
|
||||
clear_simulated_location();
|
||||
return make_response(@"ok", reqId);
|
||||
}
|
||||
|
||||
NSMutableDictionary *r = make_response(@"err", reqId);
|
||||
r[@"msg"] = [NSString stringWithFormat:@"unknown type: %@", type];
|
||||
return r;
|
||||
@@ -714,7 +896,7 @@ static BOOL handle_client(int fd) {
|
||||
@"v": @PROTOCOL_VERSION,
|
||||
@"t": @"hello",
|
||||
@"name": @"vphoned",
|
||||
@"caps": @[@"hid", @"devmode", @"file"],
|
||||
@"caps": gLocationLoaded ? @[@"hid", @"devmode", @"file", @"location"] : @[@"hid", @"devmode", @"file"],
|
||||
} mutableCopy];
|
||||
if (needUpdate) helloResp[@"need_update"] = @YES;
|
||||
|
||||
@@ -726,6 +908,7 @@ static BOOL handle_client(int fd) {
|
||||
while ((msg = read_message(fd)) != nil) {
|
||||
@autoreleasepool {
|
||||
NSString *t = msg[@"t"];
|
||||
NSLog(@"vphoned: recv cmd: %@", t);
|
||||
|
||||
if ([t isEqualToString:@"update"]) {
|
||||
NSUInteger size = [msg[@"size"] unsignedIntegerValue];
|
||||
@@ -780,6 +963,7 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
if (!load_iokit()) return 1;
|
||||
if (!load_xpc()) NSLog(@"vphoned: XPC unavailable, devmode disabled");
|
||||
load_corelocation();
|
||||
|
||||
int sock = socket(AF_VSOCK, SOCK_STREAM, 0);
|
||||
if (sock < 0) { perror("vphoned: socket(AF_VSOCK)"); return 1; }
|
||||
|
||||
26
sources/Info.plist
Normal file
26
sources/Info.plist
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.vphone.cli</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>vphone-cli</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>vphone-cli</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSLocationUsageDescription</key>
|
||||
<string>vphone-cli forwards your location to the guest VM for location simulation.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>vphone-cli forwards your location to the guest VM for location simulation.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>vphone-cli forwards your location to the guest VM for location simulation.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -9,6 +9,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var windowController: VPhoneWindowController?
|
||||
private var menuController: VPhoneMenuController?
|
||||
private var fileWindowController: VPhoneFileWindowController?
|
||||
private var locationProvider: VPhoneLocationProvider?
|
||||
private var sigintSource: DispatchSourceSignal?
|
||||
|
||||
init(cli: VPhoneCLI) {
|
||||
@@ -93,6 +94,20 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
|
||||
if FileManager.default.fileExists(atPath: vphonedURL.path) {
|
||||
control.guestBinaryURL = vphonedURL
|
||||
}
|
||||
|
||||
let provider = VPhoneLocationProvider(control: control)
|
||||
self.locationProvider = provider
|
||||
control.onConnect = { [weak provider] caps in
|
||||
if caps.contains("location") {
|
||||
provider?.startForwarding()
|
||||
} else {
|
||||
print("[location] guest does not support location simulation")
|
||||
}
|
||||
}
|
||||
control.onDisconnect = { [weak provider] in
|
||||
provider?.stopForwarding()
|
||||
}
|
||||
|
||||
if let device = vm.virtualMachine.socketDevices.first as? VZVirtioSocketDevice {
|
||||
control.connect(device: device)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ class VPhoneControl {
|
||||
/// Path to the signed vphoned binary. When set, enables auto-update.
|
||||
var guestBinaryURL: URL?
|
||||
|
||||
/// Called when guest is ready (not updating). Receives guest capabilities.
|
||||
var onConnect: (([String]) -> Void)?
|
||||
|
||||
/// Called when the guest disconnects (before reconnect attempt).
|
||||
var onDisconnect: (() -> Void)?
|
||||
|
||||
private var guestBinaryData: Data?
|
||||
private var guestBinaryHash: String?
|
||||
private var nextRequestId: UInt64 = 0
|
||||
@@ -168,6 +174,7 @@ class VPhoneControl {
|
||||
self.pushUpdate(fd: fd)
|
||||
} else {
|
||||
self.startReadLoop(fd: fd)
|
||||
self.onConnect?(caps)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,6 +387,38 @@ class VPhoneControl {
|
||||
_ = try await sendRequest(["t": "file_rename", "from": from, "to": to])
|
||||
}
|
||||
|
||||
// MARK: - Location
|
||||
|
||||
func sendLocation(latitude: Double, longitude: Double, altitude: Double,
|
||||
horizontalAccuracy: Double, verticalAccuracy: Double,
|
||||
speed: Double, course: Double) {
|
||||
nextRequestId += 1
|
||||
let msg: [String: Any] = [
|
||||
"v": Self.protocolVersion,
|
||||
"t": "location",
|
||||
"id": String(nextRequestId, radix: 16),
|
||||
"lat": latitude,
|
||||
"lon": longitude,
|
||||
"alt": altitude,
|
||||
"hacc": horizontalAccuracy,
|
||||
"vacc": verticalAccuracy,
|
||||
"speed": speed,
|
||||
"course": course,
|
||||
"ts": Date().timeIntervalSince1970,
|
||||
]
|
||||
guard let fd = connection?.fileDescriptor, writeMessage(fd: fd, dict: msg) else {
|
||||
print("[control] sendLocation failed (not connected)")
|
||||
return
|
||||
}
|
||||
print("[control] sendLocation lat=\(latitude) lon=\(longitude)")
|
||||
}
|
||||
|
||||
func sendLocationStop() {
|
||||
nextRequestId += 1
|
||||
let msg: [String: Any] = ["v": Self.protocolVersion, "t": "location_stop", "id": String(nextRequestId, radix: 16)]
|
||||
guard let fd = connection?.fileDescriptor, writeMessage(fd: fd, dict: msg) else { return }
|
||||
}
|
||||
|
||||
// MARK: - Disconnect & Reconnect
|
||||
|
||||
private func disconnect() {
|
||||
@@ -392,6 +431,10 @@ class VPhoneControl {
|
||||
// Fail all pending requests
|
||||
failAllPending()
|
||||
|
||||
if wasConnected {
|
||||
onDisconnect?()
|
||||
}
|
||||
|
||||
if wasConnected, device != nil {
|
||||
print("[control] reconnecting in 3s...")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||
|
||||
109
sources/vphone-cli/VPhoneLocationProvider.swift
Normal file
109
sources/vphone-cli/VPhoneLocationProvider.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
/// Forwards the host Mac's location to the guest VM via vsock.
|
||||
///
|
||||
/// Uses macOS CoreLocation to track the Mac's real location and forwards
|
||||
/// every update to the guest. Call `startForwarding()` when the guest
|
||||
/// reports "location" capability. Safe to call multiple times (e.g.
|
||||
/// after vphoned reconnects) — re-sends the last known position.
|
||||
@MainActor
|
||||
class VPhoneLocationProvider: NSObject {
|
||||
private let control: VPhoneControl
|
||||
private var hostModeStarted = false
|
||||
|
||||
private var locationManager: CLLocationManager?
|
||||
private var delegateProxy: LocationDelegateProxy?
|
||||
private var lastLocation: CLLocation?
|
||||
|
||||
init(control: VPhoneControl) {
|
||||
self.control = control
|
||||
super.init()
|
||||
|
||||
let proxy = LocationDelegateProxy { [weak self] location in
|
||||
Task { @MainActor in
|
||||
self?.forward(location)
|
||||
}
|
||||
}
|
||||
self.delegateProxy = proxy
|
||||
let mgr = CLLocationManager()
|
||||
mgr.delegate = proxy
|
||||
mgr.desiredAccuracy = kCLLocationAccuracyBest
|
||||
self.locationManager = mgr
|
||||
print("[location] host location forwarding ready")
|
||||
}
|
||||
|
||||
/// Begin sending location to the guest. Safe to call on every (re)connect.
|
||||
func startForwarding() {
|
||||
guard let mgr = locationManager else { return }
|
||||
mgr.requestAlwaysAuthorization()
|
||||
mgr.startUpdatingLocation()
|
||||
hostModeStarted = true
|
||||
print("[location] started host location tracking")
|
||||
// Re-send last known location immediately on reconnect
|
||||
if let last = lastLocation {
|
||||
forward(last)
|
||||
print("[location] re-sent last known host location")
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop forwarding and clear the simulated location in the guest.
|
||||
func stopForwarding() {
|
||||
if hostModeStarted {
|
||||
locationManager?.stopUpdatingLocation()
|
||||
hostModeStarted = false
|
||||
print("[location] stopped host location tracking")
|
||||
}
|
||||
}
|
||||
|
||||
private func forward(_ location: CLLocation) {
|
||||
lastLocation = location
|
||||
guard control.isConnected else {
|
||||
print("[location] forward: not connected, cached for later")
|
||||
return
|
||||
}
|
||||
control.sendLocation(
|
||||
latitude: location.coordinate.latitude,
|
||||
longitude: location.coordinate.longitude,
|
||||
altitude: location.altitude,
|
||||
horizontalAccuracy: location.horizontalAccuracy,
|
||||
verticalAccuracy: location.verticalAccuracy,
|
||||
speed: location.speed,
|
||||
course: location.course
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CLLocationManagerDelegate Proxy
|
||||
|
||||
/// Separate object to avoid @MainActor vs nonisolated delegate conflicts.
|
||||
private class LocationDelegateProxy: NSObject, CLLocationManagerDelegate {
|
||||
let handler: (CLLocation) -> Void
|
||||
|
||||
init(handler: @escaping (CLLocation) -> Void) {
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
NSLog("[location] got location: %.6f,%.6f (±%.0fm)",
|
||||
location.coordinate.latitude, location.coordinate.longitude,
|
||||
location.horizontalAccuracy)
|
||||
handler(location)
|
||||
}
|
||||
|
||||
func locationManager(_: CLLocationManager, didFailWithError error: any Error) {
|
||||
let clErr = (error as NSError).code
|
||||
// kCLErrorLocationUnknown (0) = transient, just waiting for fix
|
||||
if clErr == 0 { return }
|
||||
NSLog("[location] CLLocationManager error: %@ (code %ld)", error.localizedDescription, clErr)
|
||||
}
|
||||
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
let status = manager.authorizationStatus
|
||||
NSLog("[location] authorization status: %d", status.rawValue)
|
||||
if status == .authorized || status == .authorizedAlways {
|
||||
manager.startUpdatingLocation()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
<true/>
|
||||
<key>com.apple.vm.networking</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.location</key>
|
||||
<true/>
|
||||
<key>com.apple.security.get-task-allow</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
Reference in New Issue
Block a user