mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
fix: preserve background activity sessions
This commit is contained in:
parent
6a078be17e
commit
e7ef9491e2
7 changed files with 198 additions and 78 deletions
|
|
@ -11,7 +11,7 @@ enum AppBootstrapper {
|
||||||
static func configure() {
|
static func configure() {
|
||||||
IdeviceLogFileManager.shared.prepareForLogging()
|
IdeviceLogFileManager.shared.prepareForLogging()
|
||||||
registerDefaultSettings()
|
registerDefaultSettings()
|
||||||
startConfiguredKeepAliveServices()
|
ensureKeepAliveSelection()
|
||||||
applyDocumentPickerCopyWorkaround()
|
applyDocumentPickerCopyWorkaround()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,15 +28,10 @@ enum AppBootstrapper {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func startConfiguredKeepAliveServices() {
|
private static func ensureKeepAliveSelection() {
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
if defaults.bool(forKey: "keepAliveAudio") {
|
if !defaults.bool(forKey: "keepAliveAudio"), !defaults.bool(forKey: "keepAliveLocation") {
|
||||||
BackgroundAudioManager.shared.start()
|
|
||||||
} else if defaults.bool(forKey: "keepAliveLocation") {
|
|
||||||
BackgroundLocationManager.shared.start()
|
|
||||||
} else {
|
|
||||||
defaults.set(true, forKey: "keepAliveAudio")
|
defaults.set(true, forKey: "keepAliveAudio")
|
||||||
BackgroundAudioManager.shared.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ final class BackgroundAudioManager {
|
||||||
private var engine = AVAudioEngine()
|
private var engine = AVAudioEngine()
|
||||||
private var player = AVAudioPlayerNode()
|
private var player = AVAudioPlayerNode()
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
private var persistentEnabled = false
|
|
||||||
private var activityCount = 0
|
private var activityCount = 0
|
||||||
private var healthCheckTimer: Timer?
|
private var healthCheckTimer: Timer?
|
||||||
|
|
||||||
|
|
@ -30,13 +29,7 @@ final class BackgroundAudioManager {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func configurationDidChange() {
|
||||||
persistentEnabled = true
|
|
||||||
refreshRunningState()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stop() {
|
|
||||||
persistentEnabled = false
|
|
||||||
refreshRunningState()
|
refreshRunningState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +44,7 @@ final class BackgroundAudioManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshRunningState() {
|
private func refreshRunningState() {
|
||||||
let shouldRun = persistentEnabled || (activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveAudio"))
|
let shouldRun = activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveAudio")
|
||||||
guard shouldRun != isRunning else {
|
guard shouldRun != isRunning else {
|
||||||
if shouldRun {
|
if shouldRun {
|
||||||
recoverIfNeeded()
|
recoverIfNeeded()
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,15 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
||||||
static let shared = BackgroundLocationManager()
|
static let shared = BackgroundLocationManager()
|
||||||
|
|
||||||
private let locationManager = CLLocationManager()
|
private let locationManager = CLLocationManager()
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
private var persistentEnabled = false
|
|
||||||
private var activityCount = 0
|
private var activityCount = 0
|
||||||
|
private var isRequestingAuthorization = false
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
|
|
@ -22,13 +23,7 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
||||||
locationManager.pausesLocationUpdatesAutomatically = false
|
locationManager.pausesLocationUpdatesAutomatically = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func configurationDidChange() {
|
||||||
persistentEnabled = true
|
|
||||||
refreshRunningState()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stop() {
|
|
||||||
persistentEnabled = false
|
|
||||||
refreshRunningState()
|
refreshRunningState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,8 +37,22 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
||||||
refreshRunningState()
|
refreshRunningState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requestAuthorizationIfNeeded() {
|
||||||
|
switch locationManager.authorizationStatus {
|
||||||
|
case .authorizedAlways, .authorizedWhenInUse:
|
||||||
|
return
|
||||||
|
case .notDetermined:
|
||||||
|
isRequestingAuthorization = true
|
||||||
|
locationManager.requestAlwaysAuthorization()
|
||||||
|
case .denied, .restricted:
|
||||||
|
restoreAudioFallback()
|
||||||
|
@unknown default:
|
||||||
|
restoreAudioFallback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func refreshRunningState() {
|
private func refreshRunningState() {
|
||||||
let shouldRun = persistentEnabled || (activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveLocation"))
|
let shouldRun = activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveLocation")
|
||||||
guard shouldRun != isRunning else { return }
|
guard shouldRun != isRunning else { return }
|
||||||
|
|
||||||
isRunning = shouldRun
|
isRunning = shouldRun
|
||||||
|
|
@ -67,24 +76,47 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||||
guard isRunning else { return }
|
|
||||||
switch manager.authorizationStatus {
|
switch manager.authorizationStatus {
|
||||||
case .authorizedAlways, .authorizedWhenInUse:
|
case .authorizedAlways, .authorizedWhenInUse:
|
||||||
|
isRequestingAuthorization = false
|
||||||
|
if isRunning {
|
||||||
manager.startUpdatingLocation()
|
manager.startUpdatingLocation()
|
||||||
|
}
|
||||||
case .denied, .restricted:
|
case .denied, .restricted:
|
||||||
|
if isRunning || isRequestingAuthorization {
|
||||||
|
isRequestingAuthorization = false
|
||||||
restoreAudioFallback()
|
restoreAudioFallback()
|
||||||
|
}
|
||||||
case .notDetermined:
|
case .notDetermined:
|
||||||
return
|
return
|
||||||
@unknown default:
|
@unknown default:
|
||||||
|
if isRunning || isRequestingAuthorization {
|
||||||
|
isRequestingAuthorization = false
|
||||||
restoreAudioFallback()
|
restoreAudioFallback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func restoreAudioFallback() {
|
private func restoreAudioFallback() {
|
||||||
persistentEnabled = false
|
let defaults = UserDefaults.standard
|
||||||
isRunning = activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveLocation")
|
defaults.set(false, forKey: "keepAliveLocation")
|
||||||
UserDefaults.standard.set(true, forKey: "keepAliveAudio")
|
defaults.set(true, forKey: "keepAliveAudio")
|
||||||
BackgroundAudioManager.shared.start()
|
refreshRunningState()
|
||||||
|
BackgroundAudioManager.shared.configurationDidChange()
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
title: "Location Access Refused",
|
||||||
|
message: "Location access was refused. Open Settings to enable it.",
|
||||||
|
showOk: false,
|
||||||
|
showTryAgain: true,
|
||||||
|
primaryButtonText: "Settings"
|
||||||
|
) { openSettings in
|
||||||
|
guard openSettings,
|
||||||
|
let settingsURL = URL(string: UIApplication.openSettingsURLString) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
UIApplication.shared.open(settingsURL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||||
|
|
|
||||||
68
StikDebug/Services/LocationSimulationSession.swift
Normal file
68
StikDebug/Services/LocationSimulationSession.swift
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import Foundation
|
||||||
|
import CoreLocation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class LocationSimulationSession: ObservableObject {
|
||||||
|
static let shared = LocationSimulationSession()
|
||||||
|
|
||||||
|
@Published private(set) var isActive = false
|
||||||
|
@Published private(set) var coordinate: CLLocationCoordinate2D?
|
||||||
|
@Published private(set) var routeCoordinates: [CLLocationCoordinate2D]?
|
||||||
|
private var resendTimer: Timer?
|
||||||
|
private var routeID: UUID?
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func start(at coordinate: CLLocationCoordinate2D) {
|
||||||
|
self.coordinate = coordinate
|
||||||
|
if !isActive {
|
||||||
|
isActive = true
|
||||||
|
BackgroundAudioManager.shared.requestStart()
|
||||||
|
BackgroundLocationManager.shared.requestStart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startResending(at coordinate: CLLocationCoordinate2D, _ operation: @escaping () -> Void) {
|
||||||
|
start(at: coordinate)
|
||||||
|
resendTimer?.invalidate()
|
||||||
|
let timer = Timer(timeInterval: 4, repeats: true) { _ in operation() }
|
||||||
|
RunLoop.main.add(timer, forMode: .common)
|
||||||
|
resendTimer = timer
|
||||||
|
}
|
||||||
|
|
||||||
|
func startRoute(at coordinate: CLLocationCoordinate2D, coordinates: [CLLocationCoordinate2D]) -> UUID {
|
||||||
|
let routeID = UUID()
|
||||||
|
self.routeID = routeID
|
||||||
|
routeCoordinates = coordinates
|
||||||
|
start(at: coordinate)
|
||||||
|
return routeID
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCurrentRoute(_ routeID: UUID) -> Bool {
|
||||||
|
self.routeID == routeID
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearRoute() {
|
||||||
|
routeID = nil
|
||||||
|
routeCoordinates = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pauseResending() {
|
||||||
|
resendTimer?.invalidate()
|
||||||
|
resendTimer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateCoordinate(_ coordinate: CLLocationCoordinate2D) {
|
||||||
|
self.coordinate = coordinate
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
pauseResending()
|
||||||
|
clearRoute()
|
||||||
|
guard isActive else { return }
|
||||||
|
isActive = false
|
||||||
|
coordinate = nil
|
||||||
|
BackgroundAudioManager.shared.requestStop()
|
||||||
|
BackgroundLocationManager.shared.requestStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CoreLocation
|
||||||
|
|
||||||
private enum ExternalLocationAction: Identifiable {
|
private enum ExternalLocationAction: Identifiable {
|
||||||
case simulate(URL, Double, Double)
|
case simulate(URL, Double, Double)
|
||||||
|
|
@ -223,7 +224,22 @@ struct MainTabView: View {
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
BackgroundLocationManager.shared.requestStart()
|
LocationSimulationSession.shared.clearRoute()
|
||||||
|
LocationSimulationSession.shared.startResending(
|
||||||
|
at: CLLocationCoordinate2D(
|
||||||
|
latitude: coordinate.latitude,
|
||||||
|
longitude: coordinate.longitude
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
LocationSimulationCommandQueue.shared.async {
|
||||||
|
_ = simulate_location(
|
||||||
|
DeviceConnectionContext.targetIPAddress,
|
||||||
|
coordinate.latitude,
|
||||||
|
coordinate.longitude,
|
||||||
|
pairingFile.path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
LogManager.shared.addInfoLog(
|
LogManager.shared.addInfoLog(
|
||||||
String(format: "Simulated location from URL: %.6f, %.6f", coordinate.latitude, coordinate.longitude)
|
String(format: "Simulated location from URL: %.6f, %.6f", coordinate.latitude, coordinate.longitude)
|
||||||
)
|
)
|
||||||
|
|
@ -243,7 +259,7 @@ struct MainTabView: View {
|
||||||
let code = clear_simulated_location()
|
let code = clear_simulated_location()
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
BackgroundLocationManager.shared.requestStop()
|
LocationSimulationSession.shared.stop()
|
||||||
LogManager.shared.addInfoLog("Cleared simulated location from URL")
|
LogManager.shared.addInfoLog("Cleared simulated location from URL")
|
||||||
} else {
|
} else {
|
||||||
showAlert(
|
showAlert(
|
||||||
|
|
|
||||||
|
|
@ -724,11 +724,11 @@ final class LocationSearchCompleter: NSObject, ObservableObject, MKLocalSearchCo
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LocationSimulationView: View {
|
struct LocationSimulationView: View {
|
||||||
|
@ObservedObject private var simulationSession = LocationSimulationSession.shared
|
||||||
@State private var coordinate: CLLocationCoordinate2D?
|
@State private var coordinate: CLLocationCoordinate2D?
|
||||||
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
|
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
|
||||||
|
|
||||||
@State private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
|
@State private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
|
||||||
@State private var resendTimer: Timer?
|
|
||||||
@State private var routeLoadTask: Task<Void, Never>?
|
@State private var routeLoadTask: Task<Void, Never>?
|
||||||
@State private var routeSpeedPrefetchTask: Task<Void, Never>?
|
@State private var routeSpeedPrefetchTask: Task<Void, Never>?
|
||||||
@State private var routePlaybackTask: Task<Void, Never>?
|
@State private var routePlaybackTask: Task<Void, Never>?
|
||||||
|
|
@ -788,11 +788,11 @@ struct LocationSimulationView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hasActiveSimulation: Bool {
|
private var hasActiveSimulation: Bool {
|
||||||
simulatedCoordinate != nil || routePlaybackTask != nil
|
simulationSession.isActive || simulatedCoordinate != nil || routePlaybackTask != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isRouteRunning: Bool {
|
private var isRouteRunning: Bool {
|
||||||
routePlaybackTask != nil
|
routePlaybackTask != nil || simulationSession.routeCoordinates != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hasRouteContext: Bool {
|
private var hasRouteContext: Bool {
|
||||||
|
|
@ -801,7 +801,12 @@ struct LocationSimulationView: View {
|
||||||
routePlan != nil ||
|
routePlan != nil ||
|
||||||
isLoadingRoute ||
|
isLoadingRoute ||
|
||||||
isPrefetchingRouteSpeeds ||
|
isPrefetchingRouteSpeeds ||
|
||||||
routePlaybackCoordinate != nil
|
routePlaybackCoordinate != nil ||
|
||||||
|
simulationSession.routeCoordinates != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private var displayedRouteCoordinate: CLLocationCoordinate2D? {
|
||||||
|
simulationSession.routeCoordinates == nil ? routePlaybackCoordinate : simulationSession.coordinate
|
||||||
}
|
}
|
||||||
|
|
||||||
private var routeSummaryText: String? {
|
private var routeSummaryText: String? {
|
||||||
|
|
@ -818,6 +823,9 @@ struct LocationSimulationView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var routeStatusText: String {
|
private var routeStatusText: String {
|
||||||
|
if simulationSession.routeCoordinates != nil {
|
||||||
|
return "Route simulation active."
|
||||||
|
}
|
||||||
if isLoadingRoute {
|
if isLoadingRoute {
|
||||||
return "Calculating route…"
|
return "Calculating route…"
|
||||||
}
|
}
|
||||||
|
|
@ -892,8 +900,8 @@ struct LocationSimulationView: View {
|
||||||
Marker("End", coordinate: routeEndCoordinate)
|
Marker("End", coordinate: routeEndCoordinate)
|
||||||
.tint(.red)
|
.tint(.red)
|
||||||
}
|
}
|
||||||
if let routePlaybackCoordinate {
|
if let displayedRouteCoordinate {
|
||||||
Marker("Current", coordinate: routePlaybackCoordinate)
|
Marker("Current", coordinate: displayedRouteCoordinate)
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
}
|
}
|
||||||
} else if let coordinate {
|
} else if let coordinate {
|
||||||
|
|
@ -1025,17 +1033,28 @@ struct LocationSimulationView: View {
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadBookmarks()
|
loadBookmarks()
|
||||||
|
if let routeCoordinates = simulationSession.routeCoordinates {
|
||||||
|
coordinate = nil
|
||||||
|
routePolyline = makeRoutePolyline(for: routeCoordinates)
|
||||||
|
routeStartSelection = routeCoordinates.first.map {
|
||||||
|
RouteSearchSelection(title: "Route Start", coordinate: $0)
|
||||||
|
}
|
||||||
|
routeEndSelection = routeCoordinates.last.map {
|
||||||
|
RouteSearchSelection(title: "Route End", coordinate: $0)
|
||||||
|
}
|
||||||
|
if let routePolyline {
|
||||||
|
position = .rect(routePolyline.boundingMapRect)
|
||||||
|
}
|
||||||
|
} else if let activeCoordinate = simulationSession.coordinate {
|
||||||
|
coordinate = activeCoordinate
|
||||||
|
simulatedCoordinate = activeCoordinate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
routeLoadTask?.cancel()
|
routeLoadTask?.cancel()
|
||||||
routeLoadTask = nil
|
routeLoadTask = nil
|
||||||
routeSpeedPrefetchTask?.cancel()
|
routeSpeedPrefetchTask?.cancel()
|
||||||
routeSpeedPrefetchTask = nil
|
routeSpeedPrefetchTask = nil
|
||||||
cancelRoutePlayback(resetMarker: true)
|
|
||||||
stopResendLoop()
|
|
||||||
if backgroundTaskID != .invalid {
|
|
||||||
BackgroundLocationManager.shared.requestStop()
|
|
||||||
}
|
|
||||||
endBackgroundTask()
|
endBackgroundTask()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1297,14 +1316,14 @@ struct LocationSimulationView: View {
|
||||||
) {
|
) {
|
||||||
routePlaybackCoordinate = nil
|
routePlaybackCoordinate = nil
|
||||||
beginBackgroundTask()
|
beginBackgroundTask()
|
||||||
|
LocationSimulationSession.shared.clearRoute()
|
||||||
startResendLoop(with: coord)
|
startResendLoop(with: coord)
|
||||||
BackgroundLocationManager.shared.requestStart()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func simulateRoute() {
|
private func simulateRoute() {
|
||||||
guard pairingExists,
|
guard pairingExists,
|
||||||
routePlan != nil,
|
let routePlan,
|
||||||
let firstCoordinate = routePlaybackSamples.first?.coordinate,
|
let firstCoordinate = routePlaybackSamples.first?.coordinate,
|
||||||
!isBusy else {
|
!isBusy else {
|
||||||
return
|
return
|
||||||
|
|
@ -1319,10 +1338,13 @@ struct LocationSimulationView: View {
|
||||||
operation: { locationUpdateCode(for: firstCoordinate) }
|
operation: { locationUpdateCode(for: firstCoordinate) }
|
||||||
) {
|
) {
|
||||||
beginBackgroundTask()
|
beginBackgroundTask()
|
||||||
BackgroundLocationManager.shared.requestStart()
|
let routeID = LocationSimulationSession.shared.startRoute(
|
||||||
|
at: firstCoordinate,
|
||||||
|
coordinates: routePlan.displayCoordinates
|
||||||
|
)
|
||||||
simulatedCoordinate = nil
|
simulatedCoordinate = nil
|
||||||
routePlaybackCoordinate = firstCoordinate
|
routePlaybackCoordinate = firstCoordinate
|
||||||
startRoutePlayback()
|
startRoutePlayback(routeID: routeID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1356,13 +1378,14 @@ struct LocationSimulationView: View {
|
||||||
routeSpeedPrefetchTask = nil
|
routeSpeedPrefetchTask = nil
|
||||||
cancelRoutePlayback(resetMarker: true)
|
cancelRoutePlayback(resetMarker: true)
|
||||||
stopResendLoop()
|
stopResendLoop()
|
||||||
|
LocationSimulationSession.shared.clearRoute()
|
||||||
runLocationCommand(
|
runLocationCommand(
|
||||||
errorTitle: "Clear Failed",
|
errorTitle: "Clear Failed",
|
||||||
errorMessage: { code in "Could not clear simulated location (error \(code))." },
|
errorMessage: { code in "Could not clear simulated location (error \(code))." },
|
||||||
operation: clear_simulated_location
|
operation: clear_simulated_location
|
||||||
) {
|
) {
|
||||||
endBackgroundTask()
|
endBackgroundTask()
|
||||||
BackgroundLocationManager.shared.requestStop()
|
LocationSimulationSession.shared.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1379,18 +1402,15 @@ struct LocationSimulationView: View {
|
||||||
|
|
||||||
private func startResendLoop(with coordinate: CLLocationCoordinate2D) {
|
private func startResendLoop(with coordinate: CLLocationCoordinate2D) {
|
||||||
simulatedCoordinate = coordinate
|
simulatedCoordinate = coordinate
|
||||||
resendTimer?.invalidate()
|
LocationSimulationSession.shared.startResending(at: coordinate) {
|
||||||
resendTimer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { _ in
|
|
||||||
guard let simulatedCoordinate else { return }
|
|
||||||
LocationSimulationCommandQueue.shared.async {
|
LocationSimulationCommandQueue.shared.async {
|
||||||
_ = locationUpdateCode(for: simulatedCoordinate)
|
_ = locationUpdateCode(for: coordinate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopResendLoop() {
|
private func stopResendLoop() {
|
||||||
resendTimer?.invalidate()
|
LocationSimulationSession.shared.pauseResending()
|
||||||
resendTimer = nil
|
|
||||||
simulatedCoordinate = nil
|
simulatedCoordinate = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1520,15 +1540,19 @@ struct LocationSimulationView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startRoutePlayback() {
|
private func startRoutePlayback(routeID: UUID) {
|
||||||
routePlaybackTask = Task {
|
routePlaybackTask = Task {
|
||||||
var lastSuccessfulCoordinate = routePlaybackSamples.first?.coordinate
|
var lastSuccessfulCoordinate = routePlaybackSamples.first?.coordinate
|
||||||
|
|
||||||
for sample in routePlaybackSamples.dropFirst() {
|
for sample in routePlaybackSamples.dropFirst() {
|
||||||
try? await Task.sleep(for: .seconds(sample.delayFromPrevious))
|
try? await Task.sleep(for: .seconds(sample.delayFromPrevious))
|
||||||
guard !Task.isCancelled else { return }
|
guard !Task.isCancelled,
|
||||||
|
LocationSimulationSession.shared.isCurrentRoute(routeID) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let code = await sendLocationUpdate(for: sample.coordinate)
|
let code = await sendLocationUpdate(for: sample.coordinate)
|
||||||
|
guard LocationSimulationSession.shared.isCurrentRoute(routeID) else { return }
|
||||||
guard code == 0 else {
|
guard code == 0 else {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
routePlaybackTask = nil
|
routePlaybackTask = nil
|
||||||
|
|
@ -1546,10 +1570,12 @@ struct LocationSimulationView: View {
|
||||||
lastSuccessfulCoordinate = sample.coordinate
|
lastSuccessfulCoordinate = sample.coordinate
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
routePlaybackCoordinate = sample.coordinate
|
routePlaybackCoordinate = sample.coordinate
|
||||||
|
LocationSimulationSession.shared.updateCoordinate(sample.coordinate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
guard LocationSimulationSession.shared.isCurrentRoute(routeID) else { return }
|
||||||
routePlaybackTask = nil
|
routePlaybackTask = nil
|
||||||
if let lastSuccessfulCoordinate {
|
if let lastSuccessfulCoordinate {
|
||||||
routePlaybackCoordinate = lastSuccessfulCoordinate
|
routePlaybackCoordinate = lastSuccessfulCoordinate
|
||||||
|
|
|
||||||
|
|
@ -242,32 +242,22 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleAudioKeepAliveChange(_ enabled: Bool) {
|
private func handleAudioKeepAliveChange(_ enabled: Bool) {
|
||||||
if enabled {
|
if !enabled, !keepAliveLocation {
|
||||||
BackgroundAudioManager.shared.start()
|
|
||||||
BackgroundLocationManager.shared.stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !keepAliveLocation {
|
|
||||||
keepAliveLocation = true
|
keepAliveLocation = true
|
||||||
|
BackgroundLocationManager.shared.requestAuthorizationIfNeeded()
|
||||||
}
|
}
|
||||||
BackgroundAudioManager.shared.stop()
|
BackgroundAudioManager.shared.configurationDidChange()
|
||||||
BackgroundLocationManager.shared.start()
|
BackgroundLocationManager.shared.configurationDidChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleLocationKeepAliveChange(_ enabled: Bool) {
|
private func handleLocationKeepAliveChange(_ enabled: Bool) {
|
||||||
if enabled {
|
if enabled {
|
||||||
if !keepAliveAudio {
|
BackgroundLocationManager.shared.requestAuthorizationIfNeeded()
|
||||||
BackgroundLocationManager.shared.start()
|
} else if !keepAliveAudio {
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
BackgroundLocationManager.shared.stop()
|
|
||||||
if !keepAliveAudio {
|
|
||||||
keepAliveAudio = true
|
keepAliveAudio = true
|
||||||
BackgroundAudioManager.shared.start()
|
|
||||||
}
|
}
|
||||||
|
BackgroundAudioManager.shared.configurationDidChange()
|
||||||
|
BackgroundLocationManager.shared.configurationDidChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Business Logic
|
// MARK: - Business Logic
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue