diff --git a/apps/ios/Sources/Gateway/GatewayConnectionController.swift b/apps/ios/Sources/Gateway/GatewayConnectionController.swift index dc94f3d0797..309c488d632 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectionController.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionController.swift @@ -37,6 +37,9 @@ final class GatewayConnectionController { private(set) var pendingTrustPrompt: TrustPrompt? private let discovery = GatewayDiscoveryModel() + /// Reused instance — avoids creating CLLocationManager on the main thread + /// each time currentPermissions() is called, which triggers a UI-thread warning. + private let locationManager = CLLocationManager() private weak var appModel: NodeAppModel? private var didAutoConnect = false private var pendingServiceResolvers: [String: GatewayServiceResolver] = [:] @@ -228,15 +231,25 @@ final class GatewayConnectionController { guard let cfg = appModel.activeGatewayConnectConfig else { return } guard appModel.gatewayAutoReconnectEnabled else { return } - let refreshedConfig = GatewayConnectConfig( - url: cfg.url, - stableID: cfg.stableID, - tls: cfg.tls, - token: cfg.token, - bootstrapToken: cfg.bootstrapToken, - password: cfg.password, - nodeOptions: self.makeConnectOptions(stableID: cfg.stableID)) - appModel.applyGatewayConnectConfig(refreshedConfig) + Task { [weak self, weak appModel] in + guard let self, let appModel else { return } + let refreshedConfig = GatewayConnectConfig( + url: cfg.url, + stableID: cfg.stableID, + tls: cfg.tls, + token: cfg.token, + bootstrapToken: cfg.bootstrapToken, + password: cfg.password, + nodeOptions: await self.makeConnectOptions(stableID: cfg.stableID)) + + guard appModel.gatewayAutoReconnectEnabled, + let latestConfig = appModel.activeGatewayConnectConfig, + latestConfig.effectiveStableID == cfg.effectiveStableID, + latestConfig.url == cfg.url + else { return } + + appModel.applyGatewayConnectConfig(refreshedConfig) + } } func clearPendingTrustPrompt() { @@ -462,10 +475,10 @@ final class GatewayConnectionController { password: String?) { guard let appModel else { return } - let connectOptions = self.makeConnectOptions(stableID: gatewayStableID) - Task { [weak appModel] in - guard let appModel else { return } + Task { [weak self, weak appModel] in + guard let self, let appModel else { return } + let connectOptions = await self.makeConnectOptions(stableID: gatewayStableID) await MainActor.run { appModel.gatewayStatusText = "Connecting…" } @@ -741,7 +754,7 @@ final class GatewayConnectionController { "manual|\(host.lowercased())|\(port)" } - private func makeConnectOptions(stableID: String?) -> GatewayConnectOptions { + private func makeConnectOptions(stableID: String?) async -> GatewayConnectOptions { let defaults = UserDefaults.standard let displayName = self.resolvedDisplayName(defaults: defaults) let resolvedClientId = self.resolvedClientId(defaults: defaults, stableID: stableID) @@ -751,7 +764,7 @@ final class GatewayConnectionController { scopes: [], caps: self.currentCaps(), commands: self.currentCommands(), - permissions: self.currentPermissions(), + permissions: await self.currentPermissions(), clientId: resolvedClientId, clientMode: "node", clientDisplayName: displayName) @@ -887,13 +900,15 @@ final class GatewayConnectionController { return commands } - private func currentPermissions() -> [String: Bool] { + private func currentPermissions() async -> [String: Bool] { + let speechRecognitionStatus = await Self.currentSpeechRecognitionStatus() + var permissions: [String: Bool] = [:] permissions["camera"] = AVCaptureDevice.authorizationStatus(for: .video) == .authorized permissions["microphone"] = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized - permissions["speechRecognition"] = SFSpeechRecognizer.authorizationStatus() == .authorized + permissions["speechRecognition"] = speechRecognitionStatus == .authorized permissions["location"] = Self.isLocationAuthorized( - status: CLLocationManager().authorizationStatus) + status: self.locationManager.authorizationStatus) && CLLocationManager.locationServicesEnabled() permissions["screenRecording"] = RPScreenRecorder.shared().isAvailable @@ -921,6 +936,14 @@ final class GatewayConnectionController { return permissions } + private nonisolated static func currentSpeechRecognitionStatus() async + -> SFSpeechRecognizerAuthorizationStatus + { + await Task.detached(priority: .utility) { + SFSpeechRecognizer.authorizationStatus() + }.value + } + private static func isLocationAuthorized(status: CLAuthorizationStatus) -> Bool { switch status { case .authorizedAlways, .authorizedWhenInUse: @@ -953,8 +976,8 @@ extension GatewayConnectionController { self.currentCommands() } - func _test_currentPermissions() -> [String: Bool] { - self.currentPermissions() + func _test_currentPermissions() async -> [String: Bool] { + await self.currentPermissions() } func _test_platformString() -> String {