mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-13 18:21:27 +00:00
* fix(ios): harden watch exec approval review * fix(ios): address watch approval review feedback * fix(ios): finalize watch approval background recovery * fix(ios): finalize watch approval background recovery (#61757) (thanks @ngutman)
155 lines
5.9 KiB
Swift
155 lines
5.9 KiB
Swift
import Foundation
|
|
import OpenClawKit
|
|
|
|
enum WatchMessagingError: LocalizedError {
|
|
case unsupported
|
|
case notPaired
|
|
case watchAppNotInstalled
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .unsupported:
|
|
"WATCH_UNAVAILABLE: WatchConnectivity is not supported on this device"
|
|
case .notPaired:
|
|
"WATCH_UNAVAILABLE: no paired Apple Watch"
|
|
case .watchAppNotInstalled:
|
|
"WATCH_UNAVAILABLE: OpenClaw watch companion app is not installed"
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class WatchMessagingService: @preconcurrency WatchMessagingServicing {
|
|
private let transport: WatchConnectivityTransport
|
|
private var statusHandler: (@Sendable (WatchMessagingStatus) -> Void)?
|
|
private var lastEmittedStatus: WatchMessagingStatus?
|
|
private var replyHandler: (@Sendable (WatchQuickReplyEvent) -> Void)?
|
|
private var execApprovalResolveHandler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?
|
|
private var execApprovalSnapshotRequestHandler: (
|
|
@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?
|
|
|
|
init(transport: WatchConnectivityTransport = WatchConnectivityTransport()) {
|
|
self.transport = transport
|
|
self.transport.setStatusUpdateHandler { [weak self] snapshot in
|
|
Task { @MainActor [weak self] in
|
|
self?.emitStatusIfChanged(snapshot)
|
|
}
|
|
}
|
|
self.transport.setReplyHandler { [weak self] event in
|
|
Task { @MainActor [weak self] in
|
|
self?.emitReply(event)
|
|
}
|
|
}
|
|
self.transport.setExecApprovalResolveHandler { [weak self] event in
|
|
Task { @MainActor [weak self] in
|
|
self?.emitExecApprovalResolve(event)
|
|
}
|
|
}
|
|
self.transport.setExecApprovalSnapshotRequestHandler { [weak self] event in
|
|
Task { @MainActor [weak self] in
|
|
self?.emitExecApprovalSnapshotRequest(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
nonisolated static func isSupportedOnDevice() -> Bool {
|
|
WatchConnectivityTransport.isSupportedOnDevice()
|
|
}
|
|
|
|
nonisolated static func currentStatusSnapshot() -> WatchMessagingStatus {
|
|
WatchConnectivityTransport.currentStatusSnapshot()
|
|
}
|
|
|
|
func status() async -> WatchMessagingStatus {
|
|
await self.transport.status()
|
|
}
|
|
|
|
func setStatusHandler(_ handler: (@Sendable (WatchMessagingStatus) -> Void)?) {
|
|
self.statusHandler = handler
|
|
guard let handler else {
|
|
self.lastEmittedStatus = nil
|
|
GatewayDiagnostics.log("watch messaging: cleared status handler")
|
|
return
|
|
}
|
|
let snapshot = self.transport.currentStatusSnapshot()
|
|
self.lastEmittedStatus = snapshot
|
|
GatewayDiagnostics.log(
|
|
"watch messaging: set status handler supported=\(snapshot.supported) paired=\(snapshot.paired) appInstalled=\(snapshot.appInstalled) reachable=\(snapshot.reachable) activation=\(snapshot.activationState)")
|
|
handler(snapshot)
|
|
}
|
|
|
|
func setReplyHandler(_ handler: (@Sendable (WatchQuickReplyEvent) -> Void)?) {
|
|
self.replyHandler = handler
|
|
}
|
|
|
|
func setExecApprovalResolveHandler(_ handler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?) {
|
|
self.execApprovalResolveHandler = handler
|
|
}
|
|
|
|
func setExecApprovalSnapshotRequestHandler(
|
|
_ handler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?)
|
|
{
|
|
self.execApprovalSnapshotRequestHandler = handler
|
|
}
|
|
|
|
func sendNotification(
|
|
id: String,
|
|
params: OpenClawWatchNotifyParams) async throws -> WatchNotificationSendResult
|
|
{
|
|
let payload = WatchMessagingPayloadCodec.encodeNotificationPayload(id: id, params: params)
|
|
return try await self.transport.sendPayload(payload)
|
|
}
|
|
|
|
func sendExecApprovalPrompt(
|
|
_ message: OpenClawWatchExecApprovalPromptMessage) async throws -> WatchNotificationSendResult
|
|
{
|
|
try await self.transport.sendPayload(
|
|
WatchMessagingPayloadCodec.encodeExecApprovalPromptPayload(message))
|
|
}
|
|
|
|
func sendExecApprovalResolved(
|
|
_ message: OpenClawWatchExecApprovalResolvedMessage) async throws -> WatchNotificationSendResult
|
|
{
|
|
try await self.transport.sendPayload(
|
|
WatchMessagingPayloadCodec.encodeExecApprovalResolvedPayload(message))
|
|
}
|
|
|
|
func sendExecApprovalExpired(
|
|
_ message: OpenClawWatchExecApprovalExpiredMessage) async throws -> WatchNotificationSendResult
|
|
{
|
|
try await self.transport.sendPayload(
|
|
WatchMessagingPayloadCodec.encodeExecApprovalExpiredPayload(message))
|
|
}
|
|
|
|
func syncExecApprovalSnapshot(
|
|
_ message: OpenClawWatchExecApprovalSnapshotMessage) async throws -> WatchNotificationSendResult
|
|
{
|
|
try await self.transport.sendSnapshotPayload(
|
|
WatchMessagingPayloadCodec.encodeExecApprovalSnapshotPayload(message))
|
|
}
|
|
|
|
private func emitStatusIfChanged(_ snapshot: WatchMessagingStatus) {
|
|
guard snapshot != self.lastEmittedStatus else {
|
|
return
|
|
}
|
|
self.lastEmittedStatus = snapshot
|
|
GatewayDiagnostics.log(
|
|
"watch messaging: status supported=\(snapshot.supported) paired=\(snapshot.paired) appInstalled=\(snapshot.appInstalled) reachable=\(snapshot.reachable) activation=\(snapshot.activationState)")
|
|
self.statusHandler?(snapshot)
|
|
}
|
|
|
|
private func emitReply(_ event: WatchQuickReplyEvent) {
|
|
self.replyHandler?(event)
|
|
}
|
|
|
|
private func emitExecApprovalResolve(_ event: WatchExecApprovalResolveEvent) {
|
|
self.execApprovalResolveHandler?(event)
|
|
}
|
|
|
|
private func emitExecApprovalSnapshotRequest(_ event: WatchExecApprovalSnapshotRequestEvent) {
|
|
GatewayDiagnostics.log(
|
|
"watch messaging: snapshot request id=\(event.requestId) transport=\(event.transport) sentAtMs=\(event.sentAtMs ?? -1)")
|
|
self.execApprovalSnapshotRequestHandler?(event)
|
|
}
|
|
}
|