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 3 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.
|
|
||
| fun isAbsoluteUrl(it: String?): Boolean { | ||
| return Regex("^https?://").containsMatchIn(it.toString()) | ||
| return Regex("^https?://", RegexOption.IGNORE_CASE).containsMatchIn(it.toString()) |
| val keepScreenOnEnabled by viewModel.keepScreenOnEnabled.collectAsStateWithLifecycle() | ||
| val improvScanRequested by viewModel.improvScanRequested.collectAsStateWithLifecycle() | ||
|
|
||
| FrontendVisibleLifecycleEffect(viewModel::setFrontendVisible) |
There was a problem hiding this comment.
Look at the pattern in this file we already have a Effect high level composable where to put such Effect.
| // Dropping the cache while the page is still loading can wedge the load | ||
| _webViewActions.emit( | ||
| if (_viewState.value is FrontendViewState.Content) { | ||
| WebViewAction.HardReload() |
| WebViewAction.OpenMoreInfo(target.entityId), | ||
| ) | ||
| is FrontendTarget.Path -> externalBusRepository.send(NavigateToMessage(path = target.path)) | ||
| FrontendTarget.Default -> externalBusRepository.send(NavigateToMessage(path = "/")) |
There was a problem hiding this comment.
why not using navigateToDefaultDashboard?
| } | ||
|
|
||
| viewModelScope.launch { | ||
| // Requests are only made for servers supporting navigate, no version check is needed. |
There was a problem hiding this comment.
You are assuming that the mediator is always going to gate the version, but it might change in the future.
| // Requests are only made for servers supporting navigate, no version check is needed. | ||
| // Bus messages sent before the frontend handshake are lost, so each request waits for | ||
| // it and only the latest request is kept while waiting | ||
| webViewNavigationMediator.navigationRequests.collectLatest { target -> |
There was a problem hiding this comment.
I get your idea here, but I think to be more generic we should not rely on an in memory flow that notify this VM. Instead I would prefer to use a deep link and not restarting the LaunchActivity, and having the notification command open the deep link. This way it is more flexible, to do that we need to handle on the LaunchActivity new intent coming in, I didn't check how feasible it is.
| * Carries webview navigation requests from the server commands to the visible frontend. | ||
| */ | ||
| @Singleton | ||
| class WebViewNavigationMediator @Inject constructor() { |
There was a problem hiding this comment.
Do you really need to know if the frontend is visible??
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
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 now acts on the open frontend in place:
navigatecommand (Home Assistant 2025.6+),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 the page releases the resources it holds, like camera streams and WebRTC connections.When the frontend is not visible — or the value is something the frontend cannot show, like an absolute URL — the app is launched as before, now reusing the existing task (
FLAG_ACTIVITY_CLEAR_TOPinstead ofFLAG_ACTIVITY_MULTIPLE_TASK) so repeated commands can no longer accumulate task instances.The visible frontend publishes its server through a small mediator while resumed; publication happens on resumed rather than started so that, during instance transitions, a stopping instance can never clear the value the foreground instance just published. In-place delivery is gated on the external bus being connected: the legacy WebView falls back to a full load while the page is still booting, and the v2 frontend holds the latest request until the frontend handshake completes.
Fixes #5381
Checklist
Link to pull request in documentation repositories
User Documentation: home-assistant/companion.home-assistant#1375
Any other notes
Wired for both the legacy
WebViewActivityand theUSE_FRONTEND_V2path. Verified on-device (Samsung SM-X110): dashboard-to-dashboard navigation, more-info forentityId:targets, and reload all act on the open frontend with no relaunch; commands received while the app is closed launch a single task. Servers older than 2025.6 keep the previous launch behavior.