iOS/Gateway: wake disconnected iOS nodes via APNs before invoke (#20332)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7751f9c531
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-02-18 21:00:17 +00:00
committed by GitHub
parent 750276fa36
commit e67da1538c
8 changed files with 724 additions and 73 deletions

View File

@@ -41,6 +41,7 @@ private final class NotificationInvokeLatch<T: Sendable>: @unchecked Sendable {
@Observable
final class NodeAppModel {
private let deepLinkLogger = Logger(subsystem: "ai.openclaw.ios", category: "DeepLink")
private let pushWakeLogger = Logger(subsystem: "ai.openclaw.ios", category: "PushWake")
enum CameraHUDKind {
case photo
case recording
@@ -2125,6 +2126,15 @@ extension NodeAppModel {
await self.registerAPNsTokenIfNeeded()
}
func handleSilentPushWake(_ userInfo: [AnyHashable: Any]) async -> Bool {
guard Self.isSilentPushPayload(userInfo) else {
self.pushWakeLogger.info("Ignored APNs payload: not silent push")
return false
}
self.pushWakeLogger.info("Silent push received; attempting reconnect if needed")
return await self.reconnectGatewaySessionsForSilentPushIfNeeded()
}
func updateAPNsDeviceToken(_ tokenData: Data) {
let tokenHex = tokenData.map { String(format: "%02x", $0) }.joined()
let trimmed = tokenHex.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -2170,6 +2180,51 @@ extension NodeAppModel {
// Best-effort only.
}
}
private static func isSilentPushPayload(_ userInfo: [AnyHashable: Any]) -> Bool {
guard let apsAny = userInfo["aps"] else { return false }
if let aps = apsAny as? [AnyHashable: Any] {
return Self.hasContentAvailable(aps["content-available"])
}
if let aps = apsAny as? [String: Any] {
return Self.hasContentAvailable(aps["content-available"])
}
return false
}
private static func hasContentAvailable(_ value: Any?) -> Bool {
if let number = value as? NSNumber {
return number.intValue == 1
}
if let text = value as? String {
return text.trimmingCharacters(in: .whitespacesAndNewlines) == "1"
}
return false
}
private func reconnectGatewaySessionsForSilentPushIfNeeded() async -> Bool {
guard self.isBackgrounded else {
self.pushWakeLogger.info("Wake no-op: app not backgrounded")
return false
}
guard self.gatewayAutoReconnectEnabled else {
self.pushWakeLogger.info("Wake no-op: auto reconnect disabled")
return false
}
guard self.activeGatewayConnectConfig != nil else {
self.pushWakeLogger.info("Wake no-op: no active gateway config")
return false
}
await self.operatorGateway.disconnect()
await self.nodeGateway.disconnect()
self.operatorConnected = false
self.gatewayConnected = false
self.gatewayStatusText = "Reconnecting…"
self.talkMode.updateGatewayConnected(false)
self.pushWakeLogger.info("Wake reconnect trigger applied")
return true
}
}
#if DEBUG