mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
* Heartbeat rework * remove some debug loggings * stik mini tool poc * Saperate JITEnableContext into extensions and put each feature into their own files. Fix local pairing file is removed after switched to external device by directly loading the remote device's pairing file in place. * custom origin & xmlhttprequest * Fix some memory leak * allow background script access idevice (codes are from the old JSSupport branch) Tests are not carried out but most of the js api should work. Do we need to obfuscate some functions' name? * Fix JITEnableContext singleton initializer race condition * Fix instprox callback --------- Co-authored-by: Stephen B <158498287+StephenDev0@users.noreply.github.com>
457 lines
16 KiB
Swift
457 lines
16 KiB
Swift
//
|
|
// MapSelectionView.swift
|
|
// StikJIT
|
|
//
|
|
// Created by Stephen on 11/3/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
import MapKit
|
|
import UIKit
|
|
import Pipify
|
|
|
|
struct MapSelectionView: UIViewRepresentable {
|
|
@Binding var coordinate: CLLocationCoordinate2D?
|
|
|
|
func makeCoordinator() -> Coordinator {
|
|
Coordinator(self)
|
|
}
|
|
|
|
class Coordinator: NSObject, MKMapViewDelegate {
|
|
var parent: MapSelectionView
|
|
|
|
init(_ parent: MapSelectionView) {
|
|
self.parent = parent
|
|
}
|
|
|
|
@objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
|
|
guard gesture.state == .began,
|
|
let map = gesture.view as? MKMapView
|
|
else { return }
|
|
|
|
let point = gesture.location(in: map)
|
|
parent.coordinate = map.convert(point, toCoordinateFrom: map)
|
|
}
|
|
}
|
|
|
|
func makeUIView(context: Context) -> MKMapView {
|
|
let mapView = MKMapView(frame: .zero)
|
|
mapView.delegate = context.coordinator
|
|
|
|
let longPress = UILongPressGestureRecognizer(
|
|
target: context.coordinator,
|
|
action: #selector(Coordinator.handleLongPress(_:))
|
|
)
|
|
mapView.addGestureRecognizer(longPress)
|
|
|
|
return mapView
|
|
}
|
|
|
|
func updateUIView(_ uiView: MKMapView, context: Context) {
|
|
uiView.removeAnnotations(uiView.annotations)
|
|
|
|
if let coord = coordinate {
|
|
let annotation = MKPointAnnotation()
|
|
annotation.coordinate = coord
|
|
uiView.addAnnotation(annotation)
|
|
|
|
let region = MKCoordinateRegion(
|
|
center: coord,
|
|
latitudinalMeters: 500,
|
|
longitudinalMeters: 500
|
|
)
|
|
uiView.setRegion(region, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
final class LocationSpoofingPiPState: ObservableObject {
|
|
@Published var status: String = "Idle"
|
|
@Published var coordinate: CLLocationCoordinate2D?
|
|
@Published var lastUpdated: Date?
|
|
}
|
|
|
|
struct LocationSimulationView: View {
|
|
@Environment(\.themeExpansionManager) private var themeExpansion
|
|
@State private var coordinate: CLLocationCoordinate2D?
|
|
@State private var statusMessage: String = ""
|
|
@State private var statusIsError = false
|
|
@State private var showKeepOpenAlert = false
|
|
@State private var searchQuery = ""
|
|
@State private var searchResults: [SearchResult] = []
|
|
@State private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
|
|
@State private var resendTimer: Timer?
|
|
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
|
|
@AppStorage("enablePiP") private var enablePiP = true
|
|
@State private var pipPresented = false
|
|
@StateObject private var pipState = LocationSpoofingPiPState()
|
|
|
|
private var backgroundStyle: BackgroundStyle {
|
|
themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle
|
|
}
|
|
|
|
private var isAppStoreBuild: Bool {
|
|
#if APPSTORE
|
|
return true
|
|
#else
|
|
return false
|
|
#endif
|
|
}
|
|
|
|
private var pairingFilePath: String {
|
|
let docPathUrl = FileManager.default
|
|
.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
let currentDeviceUUIDStr = UserDefaults.standard.string(forKey: "DeviceLibraryActiveDeviceID")
|
|
|
|
let pairingFileURL: URL
|
|
if let uuid = currentDeviceUUIDStr,
|
|
uuid != "00000000-0000-0000-0000-000000000001" {
|
|
|
|
pairingFileURL = docPathUrl.appendingPathComponent(
|
|
"DeviceLibrary/Pairings/\(uuid).mobiledevicepairing"
|
|
)
|
|
} else {
|
|
pairingFileURL = docPathUrl.appendingPathComponent("pairingFile.plist")
|
|
}
|
|
|
|
return pairingFileURL.path()
|
|
}
|
|
|
|
private var pairingExists: Bool {
|
|
FileManager.default.fileExists(atPath: pairingFilePath)
|
|
}
|
|
|
|
private var deviceIP: String {
|
|
UserDefaults.standard.string(forKey: "TunnelDeviceIP") ?? "10.7.0.2"
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ZStack {
|
|
ThemedBackground(style: backgroundStyle)
|
|
.ignoresSafeArea()
|
|
|
|
ScrollView {
|
|
VStack(spacing: 20) {
|
|
searchCard
|
|
mapCard
|
|
actionsCard
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.padding(.vertical, 24)
|
|
}
|
|
|
|
if showKeepOpenAlert {
|
|
CustomErrorView(
|
|
title: "Keep StikDebug Open",
|
|
message: "Location simulation stops if the app is backgrounded. Keep StikDebug in the foreground while testing.",
|
|
onDismiss: { showKeepOpenAlert = false },
|
|
primaryButtonText: "OK",
|
|
showSecondaryButton: false,
|
|
messageType: .info
|
|
)
|
|
.transition(.opacity.combined(with: .scale))
|
|
.zIndex(1)
|
|
}
|
|
}
|
|
.navigationTitle("Location Simulator")
|
|
.onDisappear {
|
|
stopResendLoop()
|
|
endBackgroundTask()
|
|
dismissPiPSession()
|
|
}
|
|
.onChange(of: enablePiP) { _, newValue in
|
|
if !newValue {
|
|
pipPresented = false
|
|
}
|
|
}
|
|
}
|
|
.pipify(isPresented: Binding(
|
|
get: { pipPresented && enablePiP },
|
|
set: { pipPresented = $0 }
|
|
)) {
|
|
LocationSpoofingPiPView(state: pipState)
|
|
}
|
|
}
|
|
|
|
private var searchCard: some View {
|
|
MaterialCard {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Pick a Location")
|
|
.font(.headline)
|
|
.foregroundColor(.primary)
|
|
searchField
|
|
if !searchResults.isEmpty {
|
|
Divider()
|
|
ForEach(searchResults) { result in
|
|
Button(action: { select(result: result) }) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(result.name)
|
|
.font(.subheadline.weight(.semibold))
|
|
Text(result.subtitle)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.vertical, 6)
|
|
}
|
|
.buttonStyle(.plain)
|
|
if result.id != searchResults.last?.id {
|
|
Divider()
|
|
}
|
|
}
|
|
}
|
|
if !pairingExists && !isAppStoreBuild {
|
|
Label("Import a pairing file first.", systemImage: "exclamationmark.triangle")
|
|
.font(.caption)
|
|
.foregroundColor(.orange)
|
|
}
|
|
if isAppStoreBuild {
|
|
Label("Location simulation is unavailable in App Store builds.", systemImage: "nosign")
|
|
.font(.caption)
|
|
.foregroundColor(.orange)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var searchField: some View {
|
|
HStack {
|
|
Image(systemName: "magnifyingglass")
|
|
.foregroundColor(.secondary)
|
|
TextField("Search for a place", text: $searchQuery, onCommit: performSearch)
|
|
.textInputAutocapitalization(.never)
|
|
.disableAutocorrection(true)
|
|
}
|
|
.padding(12)
|
|
.background(Color(UIColor.secondarySystemBackground))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
}
|
|
|
|
private var mapCard: some View {
|
|
MaterialCard {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Map")
|
|
.font(.headline)
|
|
.foregroundColor(.primary)
|
|
MapSelectionView(coordinate: $coordinate)
|
|
.frame(height: 320)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
.stroke(Color.white.opacity(0.12), lineWidth: 1)
|
|
)
|
|
if let coord = coordinate {
|
|
Text(String(format: "%.6f, %.6f", coord.latitude, coord.longitude))
|
|
.font(.footnote)
|
|
.foregroundColor(.secondary)
|
|
} else {
|
|
Text("Long-press on the map or search above to drop a pin.")
|
|
.font(.footnote)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var actionsCard: some View {
|
|
MaterialCard {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Actions")
|
|
.font(.headline)
|
|
.foregroundColor(.primary)
|
|
HStack(spacing: 12) {
|
|
Button(action: simulate) {
|
|
Label("Simulate", systemImage: "location.fill")
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.disabled(isAppStoreBuild || coordinate == nil || !pairingExists)
|
|
|
|
Button(action: clear) {
|
|
Label("Clear", systemImage: "xmark.circle")
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.disabled(isAppStoreBuild || !pairingExists)
|
|
}
|
|
if !statusMessage.isEmpty {
|
|
Text(statusMessage)
|
|
.font(.subheadline)
|
|
.foregroundColor(statusIsError ? .red : .green)
|
|
}
|
|
Text("Device IP: \(deviceIP)")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func performSearch() {
|
|
guard !searchQuery.isEmpty else { return }
|
|
let request = MKLocalSearch.Request()
|
|
request.naturalLanguageQuery = searchQuery
|
|
MKLocalSearch(request: request).start { response, _ in
|
|
let items = response?.mapItems ?? []
|
|
searchResults = items.map { item in
|
|
SearchResult(
|
|
name: item.name ?? "Unknown",
|
|
subtitle: item.placemark.title ?? "",
|
|
item: item
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func select(result: SearchResult) {
|
|
coordinate = result.item.placemark.coordinate
|
|
statusMessage = ""
|
|
searchResults = []
|
|
searchQuery = result.name
|
|
}
|
|
|
|
private func simulate() {
|
|
guard pairingExists else {
|
|
statusMessage = "Pairing file missing."
|
|
statusIsError = true
|
|
return
|
|
}
|
|
guard let coord = coordinate else { return }
|
|
let code = simulate_location(deviceIP, coord.latitude, coord.longitude, pairingFilePath)
|
|
if code == 0 {
|
|
statusMessage = "Simulation running…"
|
|
statusIsError = false
|
|
showKeepOpenAlert = true
|
|
beginBackgroundTask()
|
|
startResendLoop()
|
|
recordPiPEvent(status: "Simulating…", coordinate: coord)
|
|
} else {
|
|
statusMessage = "Simulation failed (code \(code))."
|
|
statusIsError = true
|
|
stopResendLoop()
|
|
endBackgroundTask()
|
|
dismissPiPSession()
|
|
}
|
|
}
|
|
|
|
private func clear() {
|
|
guard pairingExists else { return }
|
|
let code = clear_simulated_location()
|
|
statusMessage = code == 0 ? "Cleared simulation." : "Clear failed (code \(code))."
|
|
statusIsError = code != 0
|
|
showKeepOpenAlert = false
|
|
stopResendLoop()
|
|
endBackgroundTask()
|
|
if code == 0 {
|
|
recordPiPEvent(status: "Simulation cleared", coordinate: nil)
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
|
dismissPiPSession()
|
|
}
|
|
} else {
|
|
dismissPiPSession()
|
|
}
|
|
}
|
|
|
|
private func beginBackgroundTask() {
|
|
guard backgroundTaskID == .invalid else { return }
|
|
backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "LocationSimulation") {
|
|
endBackgroundTask()
|
|
}
|
|
}
|
|
|
|
private func endBackgroundTask() {
|
|
guard backgroundTaskID != .invalid else { return }
|
|
UIApplication.shared.endBackgroundTask(backgroundTaskID)
|
|
backgroundTaskID = .invalid
|
|
}
|
|
|
|
private func startResendLoop() {
|
|
resendTimer?.invalidate()
|
|
resendTimer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { _ in
|
|
guard self.pairingExists, let coord = self.coordinate else { return }
|
|
_ = simulate_location(self.deviceIP, coord.latitude, coord.longitude, self.pairingFilePath)
|
|
self.recordPiPEvent(status: "Location refreshed", coordinate: coord)
|
|
}
|
|
if let coord = coordinate {
|
|
recordPiPEvent(status: "Simulating…", coordinate: coord)
|
|
}
|
|
}
|
|
|
|
private func stopResendLoop() {
|
|
resendTimer?.invalidate()
|
|
resendTimer = nil
|
|
}
|
|
|
|
private func recordPiPEvent(status: String, coordinate: CLLocationCoordinate2D?) {
|
|
DispatchQueue.main.async {
|
|
pipState.status = status
|
|
pipState.coordinate = coordinate
|
|
pipState.lastUpdated = Date()
|
|
pipPresented = true
|
|
}
|
|
}
|
|
|
|
private func dismissPiPSession() {
|
|
DispatchQueue.main.async {
|
|
pipPresented = false
|
|
pipState.lastUpdated = nil
|
|
pipState.coordinate = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct SearchResult: Identifiable {
|
|
let id = UUID()
|
|
let name: String
|
|
let subtitle: String
|
|
let item: MKMapItem
|
|
}
|
|
|
|
private struct LocationSpoofingPiPView: View {
|
|
@ObservedObject var state: LocationSpoofingPiPState
|
|
|
|
private static let relativeFormatter: RelativeDateTimeFormatter = {
|
|
let formatter = RelativeDateTimeFormatter()
|
|
formatter.unitsStyle = .short
|
|
return formatter
|
|
}()
|
|
|
|
private var coordinateText: String? {
|
|
guard let coordinate = state.coordinate else { return nil }
|
|
return String(format: "%.5f, %.5f", coordinate.latitude, coordinate.longitude)
|
|
}
|
|
|
|
private var lastUpdatedText: String? {
|
|
guard let lastUpdated = state.lastUpdated else { return nil }
|
|
let label = Self.relativeFormatter.localizedString(for: lastUpdated, relativeTo: Date())
|
|
return "Last send \(label)"
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text(state.status)
|
|
.font(.headline)
|
|
.foregroundColor(.white)
|
|
if let coordinateText {
|
|
Text(coordinateText)
|
|
.font(.subheadline.monospaced())
|
|
.foregroundColor(.white.opacity(0.9))
|
|
}
|
|
if let lastUpdatedText {
|
|
Text(lastUpdatedText)
|
|
.font(.caption)
|
|
.foregroundColor(.white.opacity(0.8))
|
|
}
|
|
Spacer()
|
|
Text("Location Spoofing")
|
|
.font(.caption2)
|
|
.foregroundColor(.white.opacity(0.7))
|
|
}
|
|
.padding()
|
|
.frame(width: 280, height: 150, alignment: .leading)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
.fill(Color.black.opacity(0.65))
|
|
)
|
|
}
|
|
}
|