Skip to content

Commit 67edc77

Browse files
authored
iOS: gate capabilities by permissions and add settings controls (#22135)
Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: 92c2660 Co-authored-by: mbelinky <[email protected]> Co-authored-by: mbelinky <[email protected]> Reviewed-by: @mbelinky
1 parent 39816e6 commit 67edc77

File tree

8 files changed

+959
-384
lines changed

8 files changed

+959
-384
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
99
- Channels/CLI: add per-account/channel `defaultTo` outbound routing fallback so `openclaw agent --deliver` can send without explicit `--reply-to` when a default target is configured. (#16985) Thanks @KirillShchetinin.
1010
- iOS/Chat: clean chat UI noise by stripping inbound untrusted metadata/timestamp prefixes, formatting tool outputs into concise summaries/errors, compacting the composer while typing, and supporting tap-to-dismiss keyboard in chat view. (#22122) thanks @mbelinky.
1111
- iOS/Watch: bridge mirrored watch prompt notification actions into iOS quick-reply handling, including queued action handoff until app model initialization. (#22123) thanks @mbelinky.
12+
- iOS/Permissions: gate advertised iOS node capabilities/commands by live OS permission state (photos/contacts/calendar/reminders/motion), add Settings permission controls and disclosure, and refresh active gateway registration after permission-driven settings changes. (#22135) thanks @mbelinky.
1213
- iOS/Tests: cover IPv4-mapped IPv6 loopback in manual TLS policy tests for connect validation paths. (#22045) Thanks @mbelinky.
1314
- iOS/Gateway: stabilize background wake and reconnect behavior with background reconnect suppression/lease windows, BGAppRefresh wake fallback, location wake hook throttling, and APNs wake retry+nudge instrumentation. (#21226) thanks @mbelinky.
1415
- Auto-reply/UI: add model fallback lifecycle visibility in verbose logs, /status active-model context with fallback reason, and cohesive WebUI fallback indicators. (#20704) Thanks @joshavant.

apps/ios/Sources/Gateway/GatewayConnectionController.swift

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import AVFoundation
2-
import Contacts
32
import CoreLocation
43
import CoreMotion
54
import CryptoKit
6-
import EventKit
75
import Foundation
86
import Darwin
97
import OpenClawKit
108
import Network
119
import Observation
12-
import Photos
1310
import ReplayKit
1411
import Security
1512
import Speech
@@ -704,7 +701,7 @@ final class GatewayConnectionController {
704701
var addr = in_addr()
705702
let parsed = host.withCString { inet_pton(AF_INET, $0, &addr) == 1 }
706703
guard parsed else { return false }
707-
let value = ntohl(addr.s_addr)
704+
let value = UInt32(bigEndian: addr.s_addr)
708705
let firstOctet = UInt8((value >> 24) & 0xFF)
709706
return firstOctet == 127
710707
}
@@ -783,6 +780,7 @@ final class GatewayConnectionController {
783780
}
784781

785782
private func currentCaps() -> [String] {
783+
let permissionSnapshot = IOSPermissionCenter.statusSnapshot()
786784
var caps = [OpenClawCapability.canvas.rawValue, OpenClawCapability.screen.rawValue]
787785

788786
// Default-on: if the key doesn't exist yet, treat it as enabled.
@@ -803,18 +801,27 @@ final class GatewayConnectionController {
803801
if WatchMessagingService.isSupportedOnDevice() {
804802
caps.append(OpenClawCapability.watch.rawValue)
805803
}
806-
caps.append(OpenClawCapability.photos.rawValue)
807-
caps.append(OpenClawCapability.contacts.rawValue)
808-
caps.append(OpenClawCapability.calendar.rawValue)
809-
caps.append(OpenClawCapability.reminders.rawValue)
810-
if Self.motionAvailable() {
804+
if permissionSnapshot.photosAllowed {
805+
caps.append(OpenClawCapability.photos.rawValue)
806+
}
807+
if permissionSnapshot.contactsAllowed {
808+
caps.append(OpenClawCapability.contacts.rawValue)
809+
}
810+
if permissionSnapshot.calendarReadAllowed || permissionSnapshot.calendarWriteAllowed {
811+
caps.append(OpenClawCapability.calendar.rawValue)
812+
}
813+
if permissionSnapshot.remindersReadAllowed || permissionSnapshot.remindersWriteAllowed {
814+
caps.append(OpenClawCapability.reminders.rawValue)
815+
}
816+
if Self.motionAvailable() && permissionSnapshot.motionAllowed {
811817
caps.append(OpenClawCapability.motion.rawValue)
812818
}
813819

814820
return caps
815821
}
816822

817823
private func currentCommands() -> [String] {
824+
let permissionSnapshot = IOSPermissionCenter.statusSnapshot()
818825
var commands: [String] = [
819826
OpenClawCanvasCommand.present.rawValue,
820827
OpenClawCanvasCommand.hide.rawValue,
@@ -858,12 +865,20 @@ final class GatewayConnectionController {
858865
commands.append(OpenClawContactsCommand.add.rawValue)
859866
}
860867
if caps.contains(OpenClawCapability.calendar.rawValue) {
861-
commands.append(OpenClawCalendarCommand.events.rawValue)
862-
commands.append(OpenClawCalendarCommand.add.rawValue)
868+
if permissionSnapshot.calendarReadAllowed {
869+
commands.append(OpenClawCalendarCommand.events.rawValue)
870+
}
871+
if permissionSnapshot.calendarWriteAllowed {
872+
commands.append(OpenClawCalendarCommand.add.rawValue)
873+
}
863874
}
864875
if caps.contains(OpenClawCapability.reminders.rawValue) {
865-
commands.append(OpenClawRemindersCommand.list.rawValue)
866-
commands.append(OpenClawRemindersCommand.add.rawValue)
876+
if permissionSnapshot.remindersReadAllowed {
877+
commands.append(OpenClawRemindersCommand.list.rawValue)
878+
}
879+
if permissionSnapshot.remindersWriteAllowed {
880+
commands.append(OpenClawRemindersCommand.add.rawValue)
881+
}
867882
}
868883
if caps.contains(OpenClawCapability.motion.rawValue) {
869884
commands.append(OpenClawMotionCommand.activity.rawValue)
@@ -874,6 +889,7 @@ final class GatewayConnectionController {
874889
}
875890

876891
private func currentPermissions() -> [String: Bool] {
892+
let permissionSnapshot = IOSPermissionCenter.statusSnapshot()
877893
var permissions: [String: Bool] = [:]
878894
permissions["camera"] = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
879895
permissions["microphone"] = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
@@ -883,22 +899,23 @@ final class GatewayConnectionController {
883899
&& CLLocationManager.locationServicesEnabled()
884900
permissions["screenRecording"] = RPScreenRecorder.shared().isAvailable
885901

886-
let photoStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
887-
permissions["photos"] = photoStatus == .authorized || photoStatus == .limited
888-
let contactsStatus = CNContactStore.authorizationStatus(for: .contacts)
889-
permissions["contacts"] = contactsStatus == .authorized || contactsStatus == .limited
890-
891-
let calendarStatus = EKEventStore.authorizationStatus(for: .event)
892-
permissions["calendar"] =
893-
calendarStatus == .authorized || calendarStatus == .fullAccess || calendarStatus == .writeOnly
894-
let remindersStatus = EKEventStore.authorizationStatus(for: .reminder)
895-
permissions["reminders"] =
896-
remindersStatus == .authorized || remindersStatus == .fullAccess || remindersStatus == .writeOnly
897-
898-
let motionStatus = CMMotionActivityManager.authorizationStatus()
899-
let pedometerStatus = CMPedometer.authorizationStatus()
900-
permissions["motion"] =
901-
motionStatus == .authorized || pedometerStatus == .authorized
902+
permissions["photos"] = permissionSnapshot.photosAllowed
903+
permissions["photosDenied"] = permissionSnapshot.photos.isDeniedOrRestricted
904+
permissions["contacts"] = permissionSnapshot.contactsAllowed
905+
permissions["contactsDenied"] = permissionSnapshot.contacts.isDeniedOrRestricted
906+
907+
permissions["calendar"] = permissionSnapshot.calendarReadAllowed || permissionSnapshot.calendarWriteAllowed
908+
permissions["calendarRead"] = permissionSnapshot.calendarReadAllowed
909+
permissions["calendarWrite"] = permissionSnapshot.calendarWriteAllowed
910+
permissions["calendarDenied"] = permissionSnapshot.calendar.isDeniedOrRestricted
911+
912+
permissions["reminders"] = permissionSnapshot.remindersReadAllowed || permissionSnapshot.remindersWriteAllowed
913+
permissions["remindersRead"] = permissionSnapshot.remindersReadAllowed
914+
permissions["remindersWrite"] = permissionSnapshot.remindersWriteAllowed
915+
permissions["remindersDenied"] = permissionSnapshot.reminders.isDeniedOrRestricted
916+
917+
permissions["motion"] = permissionSnapshot.motionAllowed
918+
permissions["motionDenied"] = permissionSnapshot.motion.isDeniedOrRestricted
902919

903920
let watchStatus = WatchMessagingService.currentStatusSnapshot()
904921
permissions["watchSupported"] = watchStatus.supported

apps/ios/Sources/Info.plist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,22 @@
4444
</array>
4545
<key>NSCameraUsageDescription</key>
4646
<string>OpenClaw can capture photos or short video clips when requested via the gateway.</string>
47+
<key>NSPhotoLibraryUsageDescription</key>
48+
<string>OpenClaw can read your photo library when you ask it to share recent photos.</string>
49+
<key>NSContactsUsageDescription</key>
50+
<string>OpenClaw can read and create contacts when requested via the gateway.</string>
4751
<key>NSLocalNetworkUsageDescription</key>
4852
<string>OpenClaw discovers and connects to your OpenClaw gateway on the local network.</string>
4953
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
5054
<string>OpenClaw can share your location in the background when you enable Always.</string>
5155
<key>NSLocationWhenInUseUsageDescription</key>
5256
<string>OpenClaw uses your location when you allow location sharing.</string>
57+
<key>NSCalendarsFullAccessUsageDescription</key>
58+
<string>OpenClaw can read and add calendar events when requested via the gateway.</string>
59+
<key>NSRemindersFullAccessUsageDescription</key>
60+
<string>OpenClaw can read and add reminders when requested via the gateway.</string>
61+
<key>NSMotionUsageDescription</key>
62+
<string>OpenClaw uses Motion &amp; Fitness data for activity and pedometer commands.</string>
5363
<key>NSMicrophoneUsageDescription</key>
5464
<string>OpenClaw needs microphone access for voice wake.</string>
5565
<key>NSSpeechRecognitionUsageDescription</key>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Foundation
2+
import UIKit
3+
4+
@MainActor
5+
extension NodeAppModel {
6+
func permissionSnapshot() -> IOSPermissionSnapshot {
7+
IOSPermissionCenter.statusSnapshot()
8+
}
9+
10+
@discardableResult
11+
func requestPermission(_ permission: IOSPermissionKind) async -> IOSPermissionSnapshot {
12+
_ = await IOSPermissionCenter.request(permission)
13+
return IOSPermissionCenter.statusSnapshot()
14+
}
15+
16+
func openSystemSettings() {
17+
guard let url = URL(string: UIApplication.openSettingsURLString) else {
18+
return
19+
}
20+
UIApplication.shared.open(url)
21+
}
22+
}

0 commit comments

Comments
 (0)