Navigate and reload the frontend in place for the webview command - #7163
Navigate and reload the frontend in place for the webview command#7163markfrancisonly wants to merge 10 commits into
Conversation
The webview command relaunched the whole app for every request, even when the frontend was already open, piling up tasks and dropping the state of the page. It now navigates the open frontend in place over the external bus, opens the more info dialog for entityId: targets, and supports reload as its command to reload the frontend without cached data so the page releases the resources it holds, like camera streams and WebRTC connections. When the frontend is not visible, the app is launched like before, reusing the existing task instead of spawning a new one every time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the command_webview notification command so that, when the Home Assistant frontend is already open on the targeted server, the command navigates/reloads in-place instead of relaunching the app (preserving page state and avoiding accumulating task instances). It introduces small mediator singletons to route “navigate” / “reload” requests to the visible frontend, with fallbacks to launching the app when in-place delivery is not possible.
Changes:
- Route
command_webviewto in-place navigation (relative path /entityId:more-info) via the external bus when the frontend is visible and supported - Add an in-place
reloadoption (hard refresh: clears WebView cache then reloads) and stop creating multiple tasks on repeated launches - Add unit/Robolectric/Compose tests covering mediators, parsing, lifecycle visibility, and command routing
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| app/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt | Routes command_webview to in-place navigation/reload when possible; changes relaunch flags to reuse existing task |
| app/src/main/kotlin/io/homeassistant/companion/android/util/WebViewNavigationMediator.kt | New singleton mediator for publishing visible server + emitting navigation requests to the visible frontend |
| app/src/main/kotlin/io/homeassistant/companion/android/util/ReloadRequestMediator.kt | New singleton mediator for emitting “reload” requests to the visible frontend |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendTarget.kt | Improves parsing of entityId: targets (case/whitespace tolerant) |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt | Consumes navigation/reload mediator events and forwards them to WebView actions / external bus (frontend v2 path) |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendScreen.kt | Publishes frontend visibility via a lifecycle effect so commands can target the currently visible frontend |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/WebViewAction.kt | Adds HardReload WebView action (clear cache + reload) |
| app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt | Handles mediator events for legacy WebViewActivity path (reload + navigation delivery) |
| app/src/main/res/xml/changelog_master.xml | Adds changelog entry for improved command_webview behavior (Main + Automotive) |
| app/src/test/kotlin/io/homeassistant/companion/android/util/WebViewNavigationMediatorTest.kt | New unit tests for navigation mediator behavior |
| app/src/test/kotlin/io/homeassistant/companion/android/notifications/MessagingManagerWebViewCommandTest.kt | New Robolectric tests for command_webview routing (navigate/entity/reload/fallback cases) |
| app/src/test/kotlin/io/homeassistant/companion/android/frontend/WebViewActionTest.kt | Adds test for HardReload ordering (clear cache before reload) |
| app/src/test/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendTargetTest.kt | Adds parameterized tests for hand-typed entityId: parsing + prefix edge case |
| app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendVisibleLifecycleEffectTest.kt | New Compose test validating visibility publication across lifecycle stop/start |
| app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt | Adds tests for reload/nav mediator integration and visible-server publication behavior |
…ntend The command target is now parsed trimmed so hand-typed spaces or scheme casing cannot slip an absolute URL or launch prefix past the in-place navigation exclusions, and the entity id is percent-encoded in the more-info URL fallback. UrlUtil.isAbsoluteUrl matches the scheme ignoring case. On the v2 frontend, a reload only drops the cache once connected since doing so mid-load can wedge the page, and navigation requests wait for the frontend handshake (keeping only the latest) because bus messages sent before it are lost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
prior versions of this command accumulated in system resource exhaustion and additionally resulted in delayed nav. this version has good manners and only starts/restarts the ha companion app if closed. to compensate and complete the journey, there's a now reload command that performs a webview hard refresh or loads the app fresh |
TimoPtr
left a comment
There was a problem hiding this comment.
I would prefer using Intent to also allow opening a link without reopening the LaunchActivity and it would drop the need of this two mediators. The WebViewActivity has been removed since this morning.
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
…-navigate-bus # Conflicts: # app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt # app/src/main/res/xml/changelog_master.xml
Apply review feedback: the webview notification command now always starts LaunchActivity with a NavigateTo (or the new ReloadFrontend) deep link, using CLEAR_TOP | SINGLE_TOP so a running activity receives it through onNewIntent instead of being recreated. A new-intent deep link is handed to the shown frontend's ViewModel to act in place, or the frontend is opened at the destination; both mediators and the frontend visibility tracking are dropped. - FrontendViewModel.navigateTo gates the external bus navigation on the server version itself, falls back to a full page load at the target for servers without navigation support, and the default target goes through navigateToDefaultDashboard - reloading stays a cache-dropping reload so a stuck frontend recovers with fresh assets; a plain reload is used while the page is still loading - the UrlUtil case-insensitivity fix is dropped from this PR to be proposed separately - the legacy WebViewActivity changes are gone with its upstream removal Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks — reworked to the Intent approach in b08e2b1 (after merging
|
Apply review feedback on the in-place paths: - relative targets are normalized to a root-relative path before the frontend navigation, which resolves them against the current page (a bare "lovelace/frontdoor" ended up as /lovelace/lovelace/frontdoor, see issue home-assistant#5381); absolute URLs cannot be navigated in place at all and get a full page load instead - a navigation waiting for the frontend handshake no longer runs on the wrong server: switching servers cancels it, and the shown server is revalidated once the page is ready before acting - reloading falls back to a plain reload: a reload already destroys the current document and releases its native resources, so the application-wide cache clear of HardReload was not needed and the action is removed Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Review follow-ups in 58e0465 and 90f3a04: in-place targets are now classified — relative paths are normalized to root-relative before the frontend navigation (the |
The reload subcommand exists to recover pages serving stale cached assets, so a reload that honors the HTTP cache can re-serve exactly what it is meant to replace. HardReload returns unchanged: clearCache(true) followed by reload(), atomic and stateless. The application-wide scope of clearCache is acceptable on purpose: the app has effectively a single WebView, so the cleared cache is precisely the frontend cache the reload refreshes. A cache-mode bypass scoped to the load was evaluated and rejected — it needs a set/restore pair coupled across an asynchronous page load and a pending flag to maintain, for no practical difference in this app. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…inks A deep link delivered to the running activity fell back to the frontend unconditionally, which on full Android Automotive could replace the dedicated UI with the WebView the Play Store forbids there, diverging from the launch routing. The destination is now resolved by the ViewModel with the same Automotive policy as a launch, unit tested for the warm path, and the Automotive changelog entry is dropped. Running deep links are also held until the UI is Ready: navigating before the NavHost exists could throw, so the buffered channel becomes a conflated latest-pending value that the effect only collects once the navigation graph is up — which also prevents several buffered commands from stacking multiple frontend entries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
You mentioned that UrlUtil will move to its own PR, I don't see any. Is that expected? |
|
@dshokouhi You might remember this, but wasn't opening a new activity and not replacing the history of any open 'main' app here intentional? |
Yes this was deliberately done to handle cases of re-navigation as eventually if you keep going back and forth navigation just stops completely and the last command is just repeated. It was easy to test goin from like from Tab A > Tab B > Tab C and keep going back and forth and it eventually just stops and no matter what command you send its stuck on Tab A. Creating a new task was the only reliable method for consistent navigation using only the webview commands. A lot of users use this command for devices that cant be reached and thus want to automate opening various tabs back to back. There should be a history of issues and PRs on the topic in git. |
This was because we were reaching the max size of the backstack? I think we might then have a flag to offer one or the other, some ppl might be interested to keep the current stack and not a new activity? |
Not a backstack when using command webview it's all forward navigation that gets stuck. Or at least got stuck. Was easy to replicate after 5-10 minutes of sending commands to various tabs. Not navigating the left navigation but the top row of tabs in a dashboard. |
Back then did you know why it was getting stuck? |
no but it did feel like a caching issue where command were being received but not respected. |
Summary
command_webviewrelaunched the whole app for every request, even when the frontend was already open and showing the targeted server. Each launch created a new task (FLAG_ACTIVITY_MULTIPLE_TASK), so repeated commands piled up activity instances, and the page state — camera streams, scroll position, dialogs — was lost every time.The command is now Intent driven end to end: it always starts
LaunchActivitywith aNavigateTo(or the newReloadFrontend) deep link, usingFLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOPin the existing task. A running activity receives the deep link throughonNewIntentand hands it to the frontend shown on top, which acts on it in place; otherwise the resolved destination is opened through the navigation graph. Deep links received while the UI is still starting are held — only the latest one — until the navigation graph is ready, and a running command follows the same Automotive policy as a launch, so the dedicated Automotive UI is never replaced by the WebView.Acting in place means:
navigatecommand (Home Assistant 2025.6+, version checked where it is used), normalized to root-relative first since the frontend resolves relative paths against the current page (thelovelace/frontdoorcase of command_webview should not relaunch application #5381),entityId:target opens the more-info dialog (the prefix is matched ignoring case and surrounding spaces, since the value is typed by hand),reloadvalue reloads the frontend without cached data, so a stale page cannot be re-served from the cache and the reload releases the resources the page holds, like camera streams and WebRTC connections,Servers older than 2025.6 and commands received while the app is closed keep the launch behavior, now without accumulating task instances.
Fixes #5381
Checklist
Link to pull request in documentation repositories
User Documentation: home-assistant/companion.home-assistant#1375
Any other notes
Verified on-device (Samsung SM-X110): dashboard-to-dashboard navigation, more-info for
entityId:targets, and reload all act on the open frontend with no relaunch; commands received while the app is closed launch a single task. The legacyWebViewActivitywas removed upstream while this PR was in review, so the implementation targets the frontend screen only. TheUrlUtilcase-insensitivity fix initially included here will be proposed separately.