Skip to content

Commit 08830f1

Browse files
committed
3.1.3 WIP
1 parent 78551b8 commit 08830f1

6 files changed

Lines changed: 701 additions & 89 deletions

File tree

StikJIT/Info.plist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<string>com.stik.StikJIT.enableJIT</string>
1212
<key>CFBundleURLSchemes</key>
1313
<array>
14+
<string>stikdebug</string>
1415
<string>stikjit</string>
1516
</array>
1617
</dict>

StikJIT/Utilities/IdeviceFFIBridge.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,10 @@ private enum LocationSimulationState {
750750
}
751751
}
752752

753+
enum LocationSimulationCommandQueue {
754+
static let shared = DispatchQueue(label: "com.stik.location-sim", qos: .userInitiated)
755+
}
756+
753757
func simulate_location(_ deviceIP: String, _ latitude: Double, _ longitude: Double, _ pairingFile: String) -> Int32 {
754758
if let locationSimulation = LocationSimulationState.locationSimulation {
755759
if let ffiError = location_simulation_set(locationSimulation, latitude, longitude) {

StikJIT/Views/HomeView.swift

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,24 +133,24 @@ struct HomeView: View {
133133
}
134134
}
135135
.onOpenURL { url in
136-
guard let host = url.host() else { return }
136+
guard let host = url.host()?.lowercased() else { return }
137137
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
138138
switch host {
139139
case "enable-jit":
140140
var config = JITEnableConfiguration()
141-
if let pidStr = components?.queryItems?.first(where: { $0.name == "pid" })?.value, let pid = Int(pidStr) {
141+
if let pidStr = queryValue(["pid"], in: components), let pid = Int(pidStr) {
142142
config.pid = pid
143143
}
144-
if let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value {
144+
if let bundleId = queryValue(["bundle-id", "bundleID", "bundle_id", "bundleId"], in: components) {
145145
config.bundleID = bundleId
146146
}
147-
if let scriptBase64URL = components?.queryItems?.first(where: { $0.name == "script-data" })?.value?.removingPercentEncoding {
147+
if let scriptBase64URL = queryValue(["script-data", "scriptData", "script_data"], in: components)?.removingPercentEncoding {
148148
let base64 = base64URLToBase64(scriptBase64URL)
149149
if let scriptData = Data(base64Encoded: base64) {
150150
config.scriptData = scriptData
151151
}
152152
}
153-
if let scriptName = components?.queryItems?.first(where: { $0.name == "script-name" })?.value {
153+
if let scriptName = queryValue(["script-name", "scriptName", "script_name"], in: components) {
154154
config.scriptName = scriptName
155155
}
156156
if config.scriptData == nil, let bundleID = config.bundleID,
@@ -164,7 +164,7 @@ struct HomeView: View {
164164
pendingJITEnableConfiguration = config
165165
}
166166
case "kill-process":
167-
if let pidStr = components?.queryItems?.first(where: { $0.name == "pid" })?.value, let pid = Int(pidStr) {
167+
if let pidStr = queryValue(["pid"], in: components), let pid = Int(pidStr) {
168168
pubTunnelConnected = false
169169
startTunnelInBackground(showErrorUI: false)
170170
DispatchQueue.global(qos: .userInitiated).async {
@@ -182,7 +182,7 @@ struct HomeView: View {
182182
}
183183
}
184184
case "launch-app":
185-
if let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value {
185+
if let bundleId = queryValue(["bundle-id", "bundleID", "bundle_id", "bundleId"], in: components) {
186186
HapticFeedbackHelper.trigger()
187187
DispatchQueue.global(qos: .userInitiated).async {
188188
let _ = JITEnableContext.shared.launchAppWithoutDebug(bundleId, logger: nil)
@@ -243,6 +243,19 @@ struct HomeView: View {
243243
}
244244
}
245245

246+
private func queryValue(_ names: [String], in components: URLComponents?) -> String? {
247+
guard let queryItems = components?.queryItems else { return nil }
248+
for name in names {
249+
if let rawValue = queryItems.first(where: { $0.name.caseInsensitiveCompare(name) == .orderedSame })?.value {
250+
let value = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
251+
if !value.isEmpty {
252+
return value
253+
}
254+
}
255+
}
256+
return nil
257+
}
258+
246259
private func debugFeedbackView(_ feedback: DebugFeedback) -> some View {
247260
HStack(spacing: 10) {
248261
if feedback.isWorking {

StikJIT/Views/MainTabView.swift

Lines changed: 146 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import SwiftUI
9+
import Foundation
910

1011
private struct TabDescriptor: Identifiable {
1112
let id: String
@@ -60,6 +61,147 @@ struct MainTabView: View {
6061
}
6162
selection = ids.first ?? settingsTab.id
6263
}
64+
65+
private func handleURL(_ url: URL) {
66+
guard let host = url.host()?.lowercased() else { return }
67+
68+
switch host {
69+
case "simulate-location", "set-location":
70+
simulateLocation(from: url)
71+
case "location", "location-simulation":
72+
if coordinate(from: url) == nil {
73+
openTab(id: "location")
74+
} else {
75+
simulateLocation(from: url)
76+
}
77+
case "clear-location", "stop-location":
78+
clearSimulatedLocation()
79+
default:
80+
break
81+
}
82+
}
83+
84+
private func openTab(id: String) {
85+
if displayTabs.contains(where: { $0.id == id }) {
86+
selection = id
87+
} else if let descriptor = availableTabs.first(where: { $0.id == id }) {
88+
detachedTab = descriptor
89+
}
90+
}
91+
92+
private func simulateLocation(from url: URL) {
93+
guard let coordinate = coordinate(from: url) else {
94+
showAlert(
95+
title: "Invalid Location URL",
96+
message: "Use stikdebug://simulate-location?lat=37.3349&lon=-122.0090",
97+
showOk: true
98+
)
99+
return
100+
}
101+
102+
guard coordinateIsValid(latitude: coordinate.latitude, longitude: coordinate.longitude) else {
103+
showAlert(
104+
title: "Invalid Coordinates",
105+
message: "Latitude must be between -90 and 90. Longitude must be between -180 and 180.",
106+
showOk: true
107+
)
108+
return
109+
}
110+
111+
let pairingFile = PairingFileStore.prepareURL()
112+
guard FileManager.default.fileExists(atPath: pairingFile.path) else {
113+
showAlert(
114+
title: "Pairing File Required",
115+
message: "Import a pairing file before simulating location from a URL.",
116+
showOk: true
117+
)
118+
return
119+
}
120+
121+
LocationSimulationCommandQueue.shared.async {
122+
let code = simulate_location(
123+
DeviceConnectionContext.targetIPAddress,
124+
coordinate.latitude,
125+
coordinate.longitude,
126+
pairingFile.path
127+
)
128+
129+
DispatchQueue.main.async {
130+
if code == 0 {
131+
BackgroundLocationManager.shared.requestStart()
132+
LogManager.shared.addInfoLog(
133+
String(format: "Simulated location from URL: %.6f, %.6f", coordinate.latitude, coordinate.longitude)
134+
)
135+
} else {
136+
showAlert(
137+
title: "Location Simulation Failed",
138+
message: "Could not simulate location from URL (error \(code)). Make sure the device is connected and the DDI is mounted.",
139+
showOk: true
140+
)
141+
}
142+
}
143+
}
144+
}
145+
146+
private func clearSimulatedLocation() {
147+
LocationSimulationCommandQueue.shared.async {
148+
let code = clear_simulated_location()
149+
DispatchQueue.main.async {
150+
if code == 0 {
151+
BackgroundLocationManager.shared.requestStop()
152+
LogManager.shared.addInfoLog("Cleared simulated location from URL")
153+
} else {
154+
showAlert(
155+
title: "Clear Location Failed",
156+
message: "Could not clear simulated location from URL (error \(code)).",
157+
showOk: true
158+
)
159+
}
160+
}
161+
}
162+
}
163+
164+
private func coordinate(from url: URL) -> (latitude: Double, longitude: Double)? {
165+
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
166+
let queryItems = components?.queryItems ?? []
167+
168+
func queryValue(_ names: [String]) -> String? {
169+
for name in names {
170+
if let value = queryItems.first(where: { $0.name.caseInsensitiveCompare(name) == .orderedSame })?.value {
171+
return value
172+
}
173+
}
174+
return nil
175+
}
176+
177+
if let latitudeText = queryValue(["lat", "latitude"]),
178+
let longitudeText = queryValue(["lon", "lng", "long", "longitude"]),
179+
let latitude = Double(latitudeText.trimmingCharacters(in: .whitespacesAndNewlines)),
180+
let longitude = Double(longitudeText.trimmingCharacters(in: .whitespacesAndNewlines)) {
181+
return (latitude, longitude)
182+
}
183+
184+
let coordinateText = queryValue(["coordinate", "coordinates", "coords", "q", "ll"])
185+
?? components?.path
186+
?? ""
187+
let values = numbers(in: coordinateText)
188+
guard values.count >= 2 else { return nil }
189+
return (values[0], values[1])
190+
}
191+
192+
private func coordinateIsValid(latitude: Double, longitude: Double) -> Bool {
193+
(-90.0...90.0).contains(latitude) && (-180.0...180.0).contains(longitude)
194+
}
195+
196+
private func numbers(in text: String) -> [Double] {
197+
let pattern = #"[-+]?(?:\d+(?:\.\d*)?|\.\d+)"#
198+
guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] }
199+
let range = NSRange(text.startIndex..<text.endIndex, in: text)
200+
return regex.matches(in: text, range: range).compactMap { match in
201+
guard let matchRange = Range(match.range, in: text) else { return nil }
202+
return Double(text[matchRange])
203+
}
204+
}
63205

64206
private var displayTabs: [TabDescriptor] {
65207
var tabs = ["home", "tools"].compactMap { id in
@@ -95,11 +237,7 @@ struct MainTabView: View {
95237
}
96238
switchObserver = NotificationCenter.default.addObserver(forName: .switchToTab, object: nil, queue: .main) { note in
97239
guard let id = note.object as? String else { return }
98-
if selectedTabDescriptors.contains(where: { $0.id == id }) {
99-
selection = id
100-
} else if let descriptor = availableTabs.first(where: { $0.id == id }) {
101-
detachedTab = descriptor
102-
}
240+
openTab(id: id)
103241
}
104242
}
105243
.onDisappear {
@@ -111,6 +249,9 @@ struct MainTabView: View {
111249
.onChange(of: enabledTabIdentifiers) { _, _ in
112250
ensureSelectionIsValid()
113251
}
252+
.onOpenURL { url in
253+
handleURL(url)
254+
}
114255
.sheet(item: $detachedTab) { descriptor in
115256
NavigationStack {
116257
descriptor.builder()

0 commit comments

Comments
 (0)