Skip to content

Fix remote Live Activity token lifecycle#4671

Merged
bgoncal merged 23 commits into
home-assistant:mainfrom
bgoncal:codex/local-ios-core-fcm-prs
May 28, 2026
Merged

Fix remote Live Activity token lifecycle#4671
bgoncal merged 23 commits into
home-assistant:mainfrom
bgoncal:codex/local-ios-core-fcm-prs

Conversation

@bgoncal
Copy link
Copy Markdown
Member

@bgoncal bgoncal commented May 27, 2026

Summary

  • Observe ActivityKit activities that are started remotely via APNs push-to-start notifications.
  • Register the stored push-to-start token under live_activity_token, matching Core's expected app data key.
  • Send Live Activity token and dismissal webhooks using Core's strict schema.
  • Update Live Activity contract tests for the frozen webhook wire format.

Root Cause

APNs could start the Live Activity, but the app was not observing Activity<HALiveActivityAttributes>.activityUpdates. That meant the app did not attach its normal per-activity push-token and lifecycle observers to remotely-started activities, so Core could not receive the per-activity push token needed for subsequent updates.

bgoncal and others added 16 commits April 23, 2026 15:16
HA core no longer reads or stores these fields (removed in
home-assistant/core#166072 per code review). Sending unused data wastes
bandwidth on every app registration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Aligns with home-assistant/core#166072 where the webhook handlers were
renamed from mobile_app_live_activity_token/dismissed to
live_activity_token/dismissed to match the naming convention of all other
webhooks in the mobile_app integration (none of which carry the prefix).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Follows the rename in LiveActivityRegistry.swift from mobile_app_live_activity_*
to live_activity_* to stay consistent with HA core's webhook naming convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Now that the supports_live_activities registration fields are gone,
ActivityKit types are no longer referenced in this file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
clear_notification with a tag already ends Live Activities via
HandlerClearNotification, making end_live_activity redundant.
Removes HandlerEndLiveActivity and its registration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drops HandlerEndLiveActivityTests and all end_live_activity routing
tests. clear_notification with a tag covers the dismissal path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…re-fcm-prs

# Conflicts:
#	Sources/App/Resources/hu.lproj/Localizable.strings
#	Sources/App/Resources/ko-KR.lproj/Localizable.strings
#	Sources/App/Resources/ru.lproj/Localizable.strings
#	Sources/App/Resources/sv.lproj/Localizable.strings
#	Sources/App/Settings/DebugView.swift
#	Sources/App/Settings/LiveActivity/LiveActivitySettingsView.swift
#	Sources/App/Settings/Settings/SettingsItem.swift
@bgoncal bgoncal marked this pull request as ready for review May 27, 2026 13:51
Copilot AI review requested due to automatic review settings May 27, 2026 13:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Live Activity lifecycle handling so remotely started activities are observed and reported to Home Assistant Core using the updated webhook schema.

Changes:

  • Adds observation of ActivityKit remote Live Activity starts.
  • Changes Live Activity token/dismissal webhook payload keys to Core’s strict schema.
  • Updates contract tests for the revised webhook wire format.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
Sources/Shared/LiveActivity/LiveActivityRegistry.swift Adds remote activity observation and updates webhook payload construction.
Sources/App/AppDelegate.swift Starts the remote Live Activity observer at app launch.
Tests/Shared/LiveActivity/LiveActivityContractTests.swift Updates frozen webhook key expectations.

Comment thread Sources/Shared/LiveActivity/LiveActivityRegistry.swift
Comment on lines +270 to +271
guard entries[tag] == nil else { continue }

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

❌ Patch coverage is 0% with 15 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@4401f7c). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...ces/Shared/LiveActivity/LiveActivityRegistry.swift 0.00% 15 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4671   +/-   ##
=======================================
  Coverage        ?   43.92%           
=======================================
  Files           ?      280           
  Lines           ?    16997           
  Branches        ?        0           
=======================================
  Hits            ?     7466           
  Misses          ?     9531           
  Partials        ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@rwarner
Copy link
Copy Markdown
Contributor

rwarner commented May 27, 2026

Hey @bgoncal, found a small unrelated bug while testing on the simulator, clear_notification was being silently dropped on simulator/Catalyst (Direct interface). Different from the real-device issue this PR + core#166072 addresses. NotificationManagerLocalPushInterfaceDirect never assigns localPushManager.delegate, so silent commands get dropped. iOS doesn't fire willPresent for them and the delegate is the only fallback. The Extension path on real iPhone already handles this at line 197.

I didn't know if you'd rather fold this into this PR rather than making a whole nother one for it. Up to you:

One-line fix in NotificationManagerLocalPushInterfaceDirect.init:

self.localPushManagers = .init { [weak self] server in
    let manager = LocalPushManager(server: server)
    manager.delegate = self?.localPushDelegate   // ← add this
    let token = NotificationCenter.default.addObserver(

Verified it clearing the notification on the simulator with the local push websocket on top of all these changes and my incoming core changes.

@rwarner
Copy link
Copy Markdown
Contributor

rwarner commented May 27, 2026

Also found a second related bug. The countdownEnd: Date? in HALiveActivityAttributes.ContentState is auto-Codable, so ActivityKit decodes it with JSONDecoder's default.deferredToDate strategy (seconds since 2001-01-01). HA core emits Unix epoch seconds (and the in-app handler already constructs Date(timeIntervalSince1970:)), so the APNs-path decode would land ~31 years in the future and the countdown timer would render wrong. The in-app path bypasses this so simulator local-push doesn't hit it.

Custom init(from:) + encode(to:) on ContentState reading the value as Double then Date(timeIntervalSince1970:)

Branch: https://github.com/rwarner/iOS/tree/fix/live-activity-codable-and-direct-delegate (the second commit, ea5067b). Same question for if you want to include it in your PR here or have it be seperate

@bgoncal
Copy link
Copy Markdown
Member Author

bgoncal commented May 28, 2026

@rwarner Please open a PR for those fixes you mentioned above, I think it makes sense, even though I dislike have custom decoders, if there is no other way, thats fine

@bgoncal bgoncal merged commit d83529d into home-assistant:main May 28, 2026
9 checks passed
bgoncal pushed a commit that referenced this pull request Jun 1, 2026
## Summary

Follow-up fixes to two issues surfaced while testing #4671 end-to-end.

1. **`HALiveActivityAttributes.ContentState.countdownEnd` decoded via
Unix epoch.** ActivityKit decodes the `content-state` JSON arriving via
APNs with the default `JSONDecoder`, whose `Date` strategy is
`.deferredToDate` (seconds since the 2001 reference date). HA core sends
`countdown_end` as Unix epoch seconds, matching the documented
`data.when` / `data.when_relative` user contract and the in-app handler
that already does `Date(timeIntervalSince1970:)`. Without a manual
decode the APNs push path renders countdowns ~31 years in the future.
Adds explicit `init(from:)` and `encode(to:)` that map `countdownEnd`
via `timeIntervalSince1970`. All other fields use
`container.decodeIfPresent` so behavior is unchanged for them.

2. **`NotificationManagerLocalPushInterfaceDirect` assigns
`LocalPushManager.delegate`.** The Extension path assigns the delegate
at line 197; the Direct path (used on simulator and Mac Catalyst) never
did. On those platforms that meant silent commands such as
`clear_notification` (no alert title/body) were dropped: iOS doesn't
fire `willPresent` for content-less notifications, and the delegate is
the only fallback that routes into `commandManager`. One-line fix that
brings the Direct factory into line with the Extension's behavior.

Real-device verification (iPhone 13 Mini, iOS 26.5, paid Developer
account): chronometer countdown rendered correctly 60→0 with the Codable
fix in place. Without it the timer rendered as if `Date` were
seconds-since-2001.

## Screenshots

n/a — both fixes affect decode / message routing behavior, not UI
rendering.

## Link to pull request in Documentation repository

Documentation: home-assistant/companion.home-assistant#1303

## Any other notes

Part of the Live Activities effort tracked in home-assistant/epics#61.
Companion server PR: home-assistant/core#166072.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants