Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dfe2e6c
Move live activity settings back to root settings and add samples
bgoncal Apr 23, 2026
f796192
Reduce unnecessary info
bgoncal Apr 23, 2026
a1fb886
PR improvements
bgoncal Apr 24, 2026
292d968
Remove supports_live_activities fields from registration payload
rwarner Apr 29, 2026
4faadd4
Rename live activity webhook types to drop mobile_app_ prefix
rwarner Apr 29, 2026
9a5e0d1
Update contract tests to match renamed webhook types
rwarner Apr 29, 2026
4cc787e
Remove unused ActivityKit import from HAAPI.swift
rwarner Apr 29, 2026
3f30ee2
Remove end_live_activity command — clear_notification handles it
rwarner May 7, 2026
ce051ca
Remove end_live_activity tests — handler no longer exists
rwarner May 7, 2026
7cfa0e3
Rename live activity tags
bgoncal May 27, 2026
7ad5ed8
Merge remote-tracking branch 'origin/pr-4554' into codex/local-ios-co…
bgoncal May 27, 2026
8f22197
Merge remote-tracking branch 'origin/pr-4585' into codex/local-ios-co…
bgoncal May 27, 2026
1995434
Merge remote-tracking branch 'origin/pr-4586' into codex/local-ios-co…
bgoncal May 27, 2026
c6848ff
Merge remote-tracking branch 'origin/pr-4616' into codex/local-ios-co…
bgoncal May 27, 2026
74bc90a
Merge remote-tracking branch 'origin/pr-4670' into codex/local-ios-co…
bgoncal May 27, 2026
f81dac2
Fix remote Live Activity token lifecycle
bgoncal May 27, 2026
287d67a
Merge branch 'main' into codex/local-ios-core-fcm-prs
bgoncal May 27, 2026
1d06f66
Remove Live Activity localization churn
bgoncal May 27, 2026
7762f88
Remove settings UI changes from Live Activity PR
bgoncal May 27, 2026
914488b
Fix Live Activity registry line wrapping
bgoncal May 27, 2026
01a174b
Merge branch 'main' into codex/local-ios-core-fcm-prs
bgoncal May 27, 2026
88920e5
Merge branch 'main' into codex/local-ios-core-fcm-prs
bgoncal May 27, 2026
b836137
Potential fix for pull request finding
bgoncal May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Sources/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
Task {
await registry.startObservingPushToStartToken()
}

// Observe activities that ActivityKit starts directly from APNs push-to-start.
// The stream is infinite; this Task is kept alive for the app's lifetime.
Task {
await registry.startObservingRemoteActivityStarts()
}
}
#endif
}
Expand Down
41 changes: 27 additions & 14 deletions Sources/Shared/LiveActivity/LiveActivityRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public protocol LiveActivityRegistryProtocol: AnyObject {
func reattach() async
@available(iOS 17.2, *)
func startObservingPushToStartToken() async
@available(iOS 17.2, *)
func startObservingRemoteActivityStarts() async
}

Comment thread
bgoncal marked this conversation as resolved.
/// Thread-safe registry for active `Activity<HALiveActivityAttributes>` instances.
Expand All @@ -38,12 +40,12 @@ public actor LiveActivityRegistry: LiveActivityRegistryProtocol {
/// Webhook type for reporting a new per-activity push token to HA.
static let webhookTypeToken = "live_activity_token"
/// Keys in the token webhook request data dictionary.
static let tokenWebhookKeys: Set<String> = ["activity_id", "push_token", "apns_environment"]
static let tokenWebhookKeys: Set<String> = ["tag", "push_token"]

/// Webhook type for reporting that a Live Activity was dismissed.
static let webhookTypeDismissed = "live_activity_dismissed"
/// Keys in the dismissed webhook request data dictionary.
static let dismissedWebhookKeys: Set<String> = ["activity_id", "tag", "reason"]
static let dismissedWebhookKeys: Set<String> = ["tag"]

// MARK: - State

Expand Down Expand Up @@ -257,6 +259,24 @@ public actor LiveActivityRegistry: LiveActivityRegistryProtocol {
}
}

/// Observe activities started remotely by ActivityKit push-to-start notifications.
///
/// When APNs starts a Live Activity without launching through the notification command
/// handler, ActivityKit delivers the created activity through this stream. Attach our
/// normal push-token and lifecycle observers so Core can receive the per-activity token.
public func startObservingRemoteActivityStarts() async {
for await activity in Activity<HALiveActivityAttributes>.activityUpdates {
let tag = activity.attributes.tag
guard entries[tag] == nil else { continue }

Comment on lines +275 to +276
let observationTask = makeObservationTask(for: activity)
entries[tag] = Entry(activity: activity, observationTask: observationTask)
Current.Log.verbose(
"LiveActivityRegistry: observed remotely started activity for tag \(tag), id=\(activity.id)"
)
}
}

// MARK: - Public Helpers

/// The stored push-to-start token for inclusion in registration payloads.
Expand Down Expand Up @@ -299,7 +319,7 @@ public actor LiveActivityRegistry: LiveActivityRegistryProtocol {
Current.Log.verbose(
"LiveActivityRegistry: new push token for tag \(activity.attributes.tag)"
)
await self.reportPushToken(tokenHex, activityID: activity.id)
await self.reportPushToken(tokenHex, tag: activity.attributes.tag)
}
}

Expand All @@ -308,11 +328,7 @@ public actor LiveActivityRegistry: LiveActivityRegistryProtocol {
for await state in activity.activityStateUpdates {
switch state {
case .dismissed, .ended:
await self.reportActivityDismissed(
activityID: activity.id,
tag: activity.attributes.tag,
reason: state == .dismissed ? "user_dismissed" : "ended"
)
await self.reportActivityDismissed(tag: activity.attributes.tag)
_ = await self.remove(id: activity.attributes.tag)
return
case .active, .stale:
Expand All @@ -333,13 +349,12 @@ public actor LiveActivityRegistry: LiveActivityRegistryProtocol {

/// Report a new activity push token to all connected HA servers.
/// The token is used by the relay server to send APNs updates directly to this activity.
private func reportPushToken(_ tokenHex: String, activityID: String) async {
private func reportPushToken(_ tokenHex: String, tag: String) async {
let request = WebhookRequest(
type: Self.webhookTypeToken,
data: [
"activity_id": activityID,
"tag": tag,
"push_token": tokenHex,
"apns_environment": Current.apnsEnvironment,
]
)
for server in Current.servers.all {
Expand All @@ -349,13 +364,11 @@ public actor LiveActivityRegistry: LiveActivityRegistryProtocol {

/// Notify HA servers that the Live Activity was dismissed or ended externally.
/// This allows HA to stop sending APNs updates for this activity.
private func reportActivityDismissed(activityID: String, tag: String, reason: String) async {
private func reportActivityDismissed(tag: String) async {
let request = WebhookRequest(
type: Self.webhookTypeDismissed,
data: [
"activity_id": activityID,
"tag": tag,
"reason": reason,
]
)
for server in Current.servers.all {
Expand Down
4 changes: 2 additions & 2 deletions Tests/Shared/LiveActivity/LiveActivityContractTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ final class LiveActivityContractTests: XCTestCase {
func testTokenWebhookKeys_areFrozen() {
XCTAssertEqual(
LiveActivityRegistry.tokenWebhookKeys,
["activity_id", "push_token", "apns_environment"]
["tag", "push_token"]
)
}

Expand All @@ -117,7 +117,7 @@ final class LiveActivityContractTests: XCTestCase {
func testDismissedWebhookKeys_areFrozen() {
XCTAssertEqual(
LiveActivityRegistry.dismissedWebhookKeys,
["activity_id", "live_activity_tag", "reason"]
["tag"]
)
}

Expand Down
Loading