Add UnifiedPush support for push notifications#6621
Add UnifiedPush support for push notifications#6621sk7n4k3d wants to merge 7 commits intohome-assistant:mainfrom
Conversation
There was a problem hiding this comment.
It seems you haven't yet signed a CLA. Please do so here.
Once you do that we will be able to review and accept this pull request.
Thanks!
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
There was a problem hiding this comment.
Pull request overview
This pull request implements UnifiedPush support as an alternative push notification provider for the Home Assistant Android app. UnifiedPush allows users to receive push notifications via a distributor app (like ntfy) without relying on Google's FCM infrastructure.
Changes:
- Implements a generic
PushProviderabstraction layer withPushProviderManagerfor managing multiple push notification providers - Adds
UnifiedPushProvider,UnifiedPushManager,UnifiedPushReceiver, andUnifiedPushWorkerfor UnifiedPush integration - Extends
DeviceRegistrationwithpushUrlandpushEncryptfields to support multiple push provider types - Implements
FcmPushProviderandWebSocketPushProviderto work with the new abstraction - Adds settings UI (
ListPreference) for users to select their preferred push provider - Includes comprehensive unit tests (186 lines in UnifiedPushMessageParsingTest, 204 lines in PushProviderManagerTest)
Reviewed changes
Copilot reviewed 33 out of 36 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| gradle/libs.versions.toml | Adds unifiedpush-connector 3.0.9 dependency; includes several unexplained version downgrades |
| common/src/main/kotlin/push/ | New PushProvider interface and PushProviderManager for managing providers |
| common/src/main/kotlin/data/ | Extends DeviceRegistration and related classes with push URL and encryption fields |
| common/src/test/kotlin/push/ | Comprehensive unit tests for push provider functionality |
| app/src/main/kotlin/push/ | Implements PushProvider variants (FCM, WebSocket, UnifiedPush) |
| app/src/main/kotlin/unifiedpush/ | UnifiedPush-specific implementation (Manager, Worker, Receiver) |
| app/src/main/kotlin/settings/ | Settings integration for push provider selection |
| app/src/main/AndroidManifest.xml | Registers UnifiedPushReceiver with UnifiedPush intent filters |
| app/lint-baseline.xml | Acknowledges ExportedReceiver lint warning for UnifiedPushReceiver |
| build-logic/convention/ | Adds unifiedpush-connector with protobuf-java exclusion to handle conflicts |
eefa371 to
4caaaea
Compare
There was a problem hiding this comment.
It seems you haven't yet signed a CLA. Please do so here.
Once you do that we will be able to review and accept this pull request.
Thanks!
4caaaea to
55aa73c
Compare
55aa73c to
b18ace5
Compare
| presenter.handlePushProviderChange(value) | ||
| } | ||
| if (value == "WebSocket") { | ||
| Toast.makeText(requireContext(), commonR.string.push_provider_websocket_enabled, Toast.LENGTH_SHORT).show() |
Check failure
Code scanning / ktlint
Argument should be on a separate line (unless all arguments can fit a single line) Error
| presenter.handlePushProviderChange(value) | ||
| } | ||
| if (value == "WebSocket") { | ||
| Toast.makeText(requireContext(), commonR.string.push_provider_websocket_enabled, Toast.LENGTH_SHORT).show() |
Check failure
Code scanning / ktlint
Argument should be on a separate line (unless all arguments can fit a single line) Error
| presenter.handlePushProviderChange(value) | ||
| } | ||
| if (value == "WebSocket") { | ||
| Toast.makeText(requireContext(), commonR.string.push_provider_websocket_enabled, Toast.LENGTH_SHORT).show() |
Check failure
Code scanning / ktlint
Argument should be on a separate line (unless all arguments can fit a single line) Error
| presenter.handlePushProviderChange(value) | ||
| } | ||
| if (value == "WebSocket") { | ||
| Toast.makeText(requireContext(), commonR.string.push_provider_websocket_enabled, Toast.LENGTH_SHORT).show() |
Check failure
Code scanning / ktlint
Argument should be on a separate line (unless all arguments can fit a single line) Error
| presenter.handlePushProviderChange(value) | ||
| } | ||
| if (value == "WebSocket") { | ||
| Toast.makeText(requireContext(), commonR.string.push_provider_websocket_enabled, Toast.LENGTH_SHORT).show() |
Check failure
Code scanning / ktlint
Argument should be on a separate line (unless all arguments can fit a single line) Error
| presenter.handlePushProviderChange(value) | ||
| } | ||
| if (value == "WebSocket") { | ||
| Toast.makeText(requireContext(), commonR.string.push_provider_websocket_enabled, Toast.LENGTH_SHORT).show() |
Check failure
Code scanning / ktlint
Argument should be on a separate line (unless all arguments can fit a single line) Error
| presenter.handlePushProviderChange(value) | ||
| } | ||
| if (value == "WebSocket") { | ||
| Toast.makeText(requireContext(), commonR.string.push_provider_websocket_enabled, Toast.LENGTH_SHORT).show() |
Check failure
Code scanning / ktlint
Missing newline before ")" Error
| presenter.handlePushProviderChange(value) | ||
| } | ||
| if (value == "WebSocket") { | ||
| Toast.makeText(requireContext(), commonR.string.push_provider_websocket_enabled, Toast.LENGTH_SHORT).show() |
Check failure
Code scanning / ktlint
Missing newline before ")" Error
| presenter.handlePushProviderChange(value) | ||
| } | ||
| if (value == "WebSocket") { | ||
| Toast.makeText(requireContext(), commonR.string.push_provider_websocket_enabled, Toast.LENGTH_SHORT).show() |
Check failure
Code scanning / ktlint
Exceeded max line length (120) Error
| presenter.handlePushProviderChange(value) | ||
| } | ||
| if (value == "WebSocket") { | ||
| Toast.makeText(requireContext(), commonR.string.push_provider_websocket_enabled, Toast.LENGTH_SHORT).show() |
Check failure
Code scanning / ktlint
Exceeded max line length (120) Error
|
Let's first review the first PR. |
b18ace5 to
01ffbe6
Compare
p1gp1g
left a comment
There was a problem hiding this comment.
Hey, just saw your PR: I've seen many people waiting for this feature, that's going to make some people happy 👍
I just wanted to ask a question, cf. comment on UnifiedPushReceiver file
| import timber.log.Timber | ||
|
|
||
| @AndroidEntryPoint | ||
| class UnifiedPushReceiver : MessagingReceiver() { |
There was a problem hiding this comment.
(Maintainer of UnififedPush lib)
Any reason to use MessagingReceiver instead of PushService?
PushService is recommended over MessagingReceiver. If there is a reason, we may be able to improve the PushService
There was a problem hiding this comment.
Hey @p1gp1g, thanks for the drive-by from the lib maintainer itself! No good reason — I just built on an older pattern without checking for the newer API. You're right, PushService is the way.
Migrated in 2b648b7a:
UnifiedPushReceiver→UnifiedPushServiceextendingPushService- Removed the 4 deprecated
<receiver>actions from the manifest, replaced with a non-exported<service>filtering onorg.unifiedpush.android.connector.PUSH_EVENT - Hilt injection via
@AndroidEntryPoint(the service is a properContext, so cleaner overall than the old receiver approach) - Dropped the
ExportedReceiverlint baseline entries since the service is internal-only
Also dropped the Context parameter from the overrides (the Service is the context). Still on connector 3.0.9 — the PushService API is available from 3.0.0 so no version bump needed.
Thanks again for the heads up, this is a much cleaner integration.
There was a problem hiding this comment.
Ok, great ! Happy to see that feature for home assistant 👍
Did you have to do anything server side?
There was a problem hiding this comment.
Nothing server side! The Home Assistant mobile_app integration already supports arbitrary push_url / push_token values via POST /api/mobile_app/registrations, so UnifiedPush plugs in cleanly:
- On distributor registration, we send the endpoint returned by
onNewEndpointaspush_url, leavepush_tokenempty, and setpush_encrypt=truewhen we have the public keys from the UnifiedPush connector. - HA then posts notifications directly to that URL. For encrypted payloads we reuse the same WebPush-like format the FCM path uses (AES-GCM + ECDH with the keys we gave HA at registration time), so the same server logic handles both.
- When the user switches provider (e.g. UnifiedPush → WebSocket), we re-register to overwrite the
push_url/push_token.
The one small gotcha I hit and just fixed in de7243211: HA's backend validates push_url as a real URL, so sending an empty string to clear the config yields HTTP 400. The fix falls back to the BuildConfig.PUSH_URL placeholder in that branch, which the WebSocket path was already using.
So from a HA core perspective: zero changes needed. This is purely an Android-side integration on top of the existing mobile_app push protocol.
Introduce a PushProvider interface that abstracts push notification delivery, allowing the user to choose between available providers (FCM, WebSocket) via a new "Push provider" setting under Notifications. This lays the groundwork for additional providers (e.g. UnifiedPush) without coupling the abstraction to any specific implementation. - Add PushProvider interface with name, isAvailable, isActive, register, unregister, and requiresPersistentConnection - Add PushProviderManager for provider selection and server registration - Add FcmPushProvider (full flavor) and WebSocketPushProvider - Add Dagger multibinding modules for both flavors - Add "Push provider" ListPreference in Notifications settings - Add WebsocketManager.restart() for instant provider switching - Extend DeviceRegistration with pushUrl and pushEncrypt fields - Add 28 unit tests for PushProvider and PushProviderManager Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove suspend from WebsocketManager.restart() (no suspending calls) - Remove unused context parameter from WebSocketPushProvider - Clear server-side push config when switching to WebSocket - Rethrow CancellationException in PushProviderManager and FcmPushProvider - Derive initial push provider from available providers instead of hardcoded default - Stabilize getAllProviders() ordering with sortedBy name - Fix PushProvider KDoc to reflect user-selection model - Wire handlePushProviderChange to actually call selectAndRegister - Remove dead getString/putString cases for notification_push_provider - Move push provider label resolution to Fragment using string resources Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Persist selectedPushProvider via PrefsRepository instead of in-memory variable to survive process death - Trigger server-side registration update when push provider changes by calling updateServerRegistration after selectAndRegister - Add explanatory comment for preferenceDataStore = null in SettingsFragment push provider preference - Replace hardcoded "WebSocket" fallback with WebSocketPushProvider.NAME constant reference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ment - Fix import ordering in SettingsFragment.kt and SettingsPresenterImpl.kt - Fix argument wrapping for Toast.makeText call in SettingsFragment.kt - Fix constructor parameter formatting in FcmPushProvider.kt - Clarify PushProvider KDoc: explicitly note user-configurable selection with no automatic "best provider" logic
The positional args broke when pushUrl/pushEncrypt were added to DeviceRegistration. Boolean was being passed where String? was expected. Named parameters make this resilient to parameter order.
Implement UnifiedPush as an alternative push notification provider, allowing users to receive notifications via a distributor app (e.g. ntfy) without relying on Google's FCM infrastructure. - Add UnifiedPushProvider with distributor-aware registration - Add UnifiedPushManager for registration lifecycle and retry logic - Add UnifiedPushService extending PushService (recommended over deprecated MessagingReceiver) - Add UnifiedPushWorker for background retry with backoff - Add UnifiedPush distributor selection in Push provider settings - Add IgnoreViolationRule for UnifiedPush connector StrictMode violations - Add protobuf-java exclusion to avoid conflict with protobuf-javalite - Add UnifiedPush guard in FcmPushProvider.isActive() and FCM onNewToken - Extend MessagingManager with Map<String, Any> message handler - Add 11 unit tests for UnifiedPush message parsing - Add unifiedpush-connector 3.0.9 dependency
01ffbe6 to
2b648b7
Compare
- Mobile app registration rejected with HTTP 400 on servers that validate push_url as a URL: the "clear push config" branch of createUpdateRegistrationRequest was sending an empty string for push_url. Fall back to the BuildConfig PUSH_URL placeholder (the same value used for the initial WebSocket registration) so the payload is always valid. - Settings screen crashed at inflation time with IllegalArgumentException "No string found by this key: notification_push_provider". The ListPreference was trying to read its initial value from the presenter data store before updatePushProviderPrefs() had a chance to detach it. Handle the notification_push_provider key directly in SettingsPresenterImpl.getString/putString (backed by PrefsRepository) and drop the now-unnecessary preferenceDataStore = null hack.
Summary
UnifiedPushProvider,UnifiedPushManager,UnifiedPushReceiver, andUnifiedPushWorkerIgnoreViolationRulefor UnifiedPush connector library StrictMode violations (following the existing pattern for third-party libs)FcmPushProvider.isActive()and FCMonNewToken()Depends on #6619 (PushProvider abstraction). This PR adds the UnifiedPush-specific implementation on top.
Testing
Tested on Pixel 9 Pro Fold (Android 16, GrapheneOS) with ntfy as UnifiedPush distributor:
Type of change