Revert "iOS: align node permissions and notifications"

This reverts commit b17e6fdd07.
This commit is contained in:
Mariano Belinky
2026-01-31 09:32:29 +01:00
parent ed65131c1c
commit 821ed35be1
5 changed files with 8 additions and 290 deletions
@@ -1,39 +1,14 @@
import OpenClawKit
import AVFoundation
import CoreLocation
import Darwin
import Foundation
import Network
import Observation
import ReplayKit
import SwiftUI
import UIKit
@MainActor
@Observable
final class GatewayConnectionController {
struct PermissionStatusProvider: Sendable {
var cameraStatus: @Sendable () -> AVAuthorizationStatus
var microphoneStatus: @Sendable () -> AVAuthorizationStatus
var locationStatus: @Sendable () -> CLAuthorizationStatus
var locationServicesEnabled: @Sendable () -> Bool
var screenRecordingAvailable: @Sendable () -> Bool
static func live() -> PermissionStatusProvider {
PermissionStatusProvider(
cameraStatus: { AVCaptureDevice.authorizationStatus(for: .video) },
microphoneStatus: { AVCaptureDevice.authorizationStatus(for: .audio) },
locationStatus: {
if #available(iOS 14.0, *) {
return CLLocationManager.authorizationStatus()
}
return CLLocationManager().authorizationStatus
},
locationServicesEnabled: { CLLocationManager.locationServicesEnabled() },
screenRecordingAvailable: { RPScreenRecorder.shared().isAvailable })
}
}
private(set) var gateways: [GatewayDiscoveryModel.DiscoveredGateway] = []
private(set) var discoveryStatusText: String = "Idle"
private(set) var discoveryDebugLog: [GatewayDiscoveryModel.DebugLogEntry] = []
@@ -41,15 +16,9 @@ final class GatewayConnectionController {
private let discovery = GatewayDiscoveryModel()
private weak var appModel: NodeAppModel?
private var didAutoConnect = false
private let permissionProvider: PermissionStatusProvider
init(
appModel: NodeAppModel,
startDiscovery: Bool = true,
permissionProvider: PermissionStatusProvider = PermissionStatusProvider.live())
{
init(appModel: NodeAppModel, startDiscovery: Bool = true) {
self.appModel = appModel
self.permissionProvider = permissionProvider
GatewaySettingsStore.bootstrapPersistence()
let defaults = UserDefaults.standard
@@ -313,7 +282,7 @@ final class GatewayConnectionController {
scopes: [],
caps: self.currentCaps(),
commands: self.currentCommands(),
permissions: self.currentPermissions(),
permissions: [:],
clientId: "openclaw-ios",
clientMode: "node",
clientDisplayName: displayName)
@@ -366,6 +335,10 @@ final class GatewayConnectionController {
OpenClawCanvasA2UICommand.reset.rawValue,
OpenClawScreenCommand.record.rawValue,
OpenClawSystemCommand.notify.rawValue,
OpenClawSystemCommand.which.rawValue,
OpenClawSystemCommand.run.rawValue,
OpenClawSystemCommand.execApprovalsGet.rawValue,
OpenClawSystemCommand.execApprovalsSet.rawValue,
]
let caps = Set(self.currentCaps())
@@ -381,32 +354,6 @@ final class GatewayConnectionController {
return commands
}
private func currentPermissions() -> [String: Bool] {
let camera = self.permissionProvider.cameraStatus()
let microphone = self.permissionProvider.microphoneStatus()
let locationStatus = self.permissionProvider.locationStatus()
let locationEnabled = self.permissionProvider.locationServicesEnabled()
let screenRecordingAvailable = self.permissionProvider.screenRecordingAvailable()
return [
"camera": camera == .authorized,
"microphone": microphone == .authorized,
"location": locationEnabled && Self.isLocationAuthorized(status: locationStatus),
"screenRecording": screenRecordingAvailable,
]
}
private static func isLocationAuthorized(status: CLAuthorizationStatus) -> Bool {
switch status {
case .authorizedAlways, .authorizedWhenInUse:
return true
case .authorized:
return true
default:
return false
}
}
private func platformString() -> String {
let v = ProcessInfo.processInfo.operatingSystemVersion
let name = switch UIDevice.current.userInterfaceIdiom {
@@ -460,10 +407,6 @@ extension GatewayConnectionController {
self.currentCommands()
}
func _test_currentPermissions() -> [String: Bool] {
self.currentPermissions()
}
func _test_platformString() -> String {
self.platformString()
}
+2 -117
View File
@@ -3,63 +3,6 @@ import Network
import Observation
import SwiftUI
import UIKit
import UserNotifications
enum NotificationAuthorizationStatus: Sendable {
case notDetermined
case denied
case authorized
case provisional
case ephemeral
}
protocol NotificationCentering: Sendable {
func authorizationStatus() async -> NotificationAuthorizationStatus
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool
func add(_ request: UNNotificationRequest) async throws
}
struct LiveNotificationCenter: NotificationCentering, @unchecked Sendable {
private let center: UNUserNotificationCenter
init(center: UNUserNotificationCenter = .current()) {
self.center = center
}
func authorizationStatus() async -> NotificationAuthorizationStatus {
let settings = await self.center.notificationSettings()
return switch settings.authorizationStatus {
case .authorized:
.authorized
case .provisional:
.provisional
case .ephemeral:
.ephemeral
case .denied:
.denied
case .notDetermined:
.notDetermined
@unknown default:
.denied
}
}
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool {
try await self.center.requestAuthorization(options: options)
}
func add(_ request: UNNotificationRequest) async throws {
try await withCheckedThrowingContinuation { cont in
self.center.add(request) { error in
if let error {
cont.resume(throwing: error)
} else {
cont.resume()
}
}
}
}
}
@MainActor
@Observable
@@ -85,7 +28,6 @@ final class NodeAppModel {
private let gateway = GatewayNodeSession()
private var gatewayTask: Task<Void, Never>?
private var voiceWakeSyncTask: Task<Void, Never>?
private let notificationCenter: NotificationCentering
@ObservationIgnored private var cameraHUDDismissTask: Task<Void, Never>?
let voiceWake = VoiceWakeManager()
let talkMode = TalkModeManager()
@@ -100,8 +42,7 @@ final class NodeAppModel {
var cameraFlashNonce: Int = 0
var screenRecordActive: Bool = false
init(notificationCenter: NotificationCentering = LiveNotificationCenter()) {
self.notificationCenter = notificationCenter
init() {
self.voiceWake.configure { [weak self] cmd in
guard let self else { return }
let sessionKey = await MainActor.run { self.mainSessionKey }
@@ -601,14 +542,12 @@ final class NodeAppModel {
return try await self.handleCameraInvoke(req)
case OpenClawScreenCommand.record.rawValue:
return try await self.handleScreenRecordInvoke(req)
case OpenClawSystemCommand.notify.rawValue:
return try await self.handleSystemNotify(req)
default:
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
}
}
} catch {
if command.hasPrefix("camera.") {
let text = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
@@ -689,7 +628,6 @@ final class NodeAppModel {
case OpenClawCanvasCommand.present.rawValue:
let params = (try? Self.decodeParams(OpenClawCanvasPresentParams.self, from: req.paramsJSON)) ??
OpenClawCanvasPresentParams()
// iOS ignores placement params (canvas presents full-screen).
let url = params.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if url.isEmpty {
self.screen.showDefaultCanvas()
@@ -698,7 +636,6 @@ final class NodeAppModel {
}
return BridgeInvokeResponse(id: req.id, ok: true)
case OpenClawCanvasCommand.hide.rawValue:
self.showLocalCanvasOnDisconnect()
return BridgeInvokeResponse(id: req.id, ok: true)
case OpenClawCanvasCommand.navigate.rawValue:
let params = try Self.decodeParams(OpenClawCanvasNavigateParams.self, from: req.paramsJSON)
@@ -922,58 +859,6 @@ final class NodeAppModel {
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
}
private func handleSystemNotify(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
let params = try Self.decodeParams(OpenClawSystemNotifyParams.self, from: req.paramsJSON)
let status = await self.notificationCenter.authorizationStatus()
let authorized: Bool
switch status {
case .authorized, .provisional, .ephemeral:
authorized = true
case .notDetermined:
authorized = (try await self.notificationCenter
.requestAuthorization(options: [.alert, .sound, .badge]))
case .denied:
authorized = false
}
guard authorized else {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: OpenClawNodeError(
code: .unavailable,
message: "NOTIFICATION_PERMISSION_REQUIRED: enable Notifications in Settings"))
}
let content = UNMutableNotificationContent()
content.title = params.title
content.body = params.body
let sound = params.sound?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if sound.isEmpty {
content.sound = .default
} else {
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: sound))
}
if let priority = params.priority {
switch priority {
case .passive:
content.interruptionLevel = .passive
case .active:
content.interruptionLevel = .active
case .timeSensitive:
content.interruptionLevel = .timeSensitive
}
}
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: trigger)
try await self.notificationCenter.add(request)
return BridgeInvokeResponse(id: req.id, ok: true)
}
}
private extension NodeAppModel {