Harden Apple Watch snapshot synchronization

This commit is contained in:
2026-08-17 18:06:09 +02:00
parent 5c88d3ace0
commit 2a59803b6c
8 changed files with 372 additions and 64 deletions

View File

@@ -46,6 +46,7 @@ private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
Notification.Name.ironStorageWatchSnapshotDidChange,
.ironStorageLocalStoreDidChange,
.ironStorageAuthenticationDidChange,
UIApplication.didBecomeActiveNotification,
] {
NotificationCenter.default.addObserver(
self,
@@ -103,21 +104,7 @@ private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
Self.snapshotKey: snapshot,
Self.deliveredReceiptKey: Data(transfer.deliveredReceipt),
]
do {
try session.updateApplicationContext(payload)
if session.isReachable {
session.sendMessage(payload, replyHandler: nil) { _ in
session.transferUserInfo(payload)
}
} else {
session.transferUserInfo(payload)
}
} catch {
_ = try? authentication.failWatchSnapshot(
revision: transfer.revision,
detail: "The latest Apple Watch snapshot could not be queued."
)
}
publish(payload, through: session)
notifyStatusChanged()
case .failure:
// A locked GPG key leaves the previous replacement pending. Authentication
@@ -127,6 +114,18 @@ private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
}
}
private func publish(_ payload: [String: Any], through session: WCSession) {
for transfer in session.outstandingUserInfoTransfers
where transfer.userInfo[Self.snapshotKey] != nil {
transfer.cancel()
}
try? session.updateApplicationContext(payload)
session.transferUserInfo(payload)
if session.isReachable {
session.sendMessage(payload, replyHandler: nil) { _ in }
}
}
private func recordUnavailable(unpaired: Bool, detail: String) {
guard let authentication else { return }
_ = try? authentication.setWatchSnapshotUnavailable(unpaired: unpaired, detail: detail)

View File

@@ -85,6 +85,12 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
func sceneBecameActive() {
restoreSnapshot()
guard WCSession.isSupported() else { return }
let session = WCSession.default
session.activate()
if session.activationState == .activated {
receive(session.receivedApplicationContext, session: session, authoritative: true)
}
}
func sceneBecameInactive() {
@@ -108,7 +114,11 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
}
}
private func receive(_ applicationContext: [String: Any], session: WCSession) {
private func receive(
_ applicationContext: [String: Any],
session: WCSession,
authoritative: Bool = false
) {
try? core.syncStarted()
guard var snapshot = applicationContext[Self.snapshotKey] as? Data else {
try? core.syncFinished()
@@ -117,7 +127,11 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
}
defer { snapshot.resetBytes(in: 0..<snapshot.count) }
do {
let update = try core.applySnapshot(snapshot: snapshot)
let update = if authoritative {
try core.applyAuthoritativeSnapshot(snapshot: snapshot)
} else {
try core.applySnapshot(snapshot: snapshot)
}
switch update.persistence {
case .keep:
break
@@ -128,20 +142,25 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
}
refreshPresentation()
guard !update.receipt.isEmpty else { return }
let acknowledgement = [Self.receiptKey: update.receipt]
if session.isReachable {
session.sendMessage(acknowledgement, replyHandler: nil) { _ in
session.transferUserInfo(acknowledgement)
}
} else {
session.transferUserInfo(acknowledgement)
}
acknowledge(update.receipt, through: session)
} catch {
try? core.syncFailed()
refreshPresentation()
}
}
private func acknowledge(_ receipt: Data, through session: WCSession) {
let acknowledgement = [Self.receiptKey: receipt]
for transfer in session.outstandingUserInfoTransfers
where transfer.userInfo[Self.receiptKey] != nil {
transfer.cancel()
}
session.transferUserInfo(acknowledgement)
if session.isReachable {
session.sendMessage(acknowledgement, replyHandler: nil) { _ in }
}
}
func refreshPresentation(at date: Date = .now) {
presentation = try? core.presentationAt(
unixSeconds: UInt64(date.timeIntervalSince1970)
@@ -160,7 +179,11 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
self.refreshPresentation()
return
}
self.receive(session.receivedApplicationContext, session: session)
self.receive(
session.receivedApplicationContext,
session: session,
authoritative: true
)
}
}
@@ -169,7 +192,7 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
didReceiveApplicationContext applicationContext: [String: Any]
) {
Task { @MainActor [weak self] in
self?.receive(applicationContext, session: session)
self?.receive(applicationContext, session: session, authoritative: true)
}
}