Skip to content

Navigate and reload the frontend in place for the webview command - #7163

Open
markfrancisonly wants to merge 10 commits into
home-assistant:mainfrom
markfrancisonly:fix/command-webview-navigate-bus
Open

Navigate and reload the frontend in place for the webview command#7163
markfrancisonly wants to merge 10 commits into
home-assistant:mainfrom
markfrancisonly:fix/command-webview-navigate-bus

Conversation

@markfrancisonly

@markfrancisonly markfrancisonly commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

command_webview relaunched 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 LaunchActivity with a NavigateTo (or the new ReloadFrontend) deep link, using FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP in the existing task. A running activity receives the deep link through onNewIntent and 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:

  • a path is sent over the external bus using the navigate command (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 (the lovelace/frontdoor case of command_webview should not relaunch application #5381),
  • an entityId: target opens the more-info dialog (the prefix is matched ignoring case and surrounding spaces, since the value is typed by hand),
  • a new reload value 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,
  • an absolute URL, or a server without navigation support, gets a full page load instead,
  • a request still waiting for the frontend handshake is dropped when the user switches servers meanwhile, so a command can never land on the wrong server.

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

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.

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 legacy WebViewActivity was removed upstream while this PR was in review, so the implementation targets the frontend screen only. The UrlUtil case-insensitivity fix initially included here will be proposed separately.

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>
Copilot AI review requested due to automatic review settings July 11, 2026 17:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 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_webview to in-place navigation (relative path / entityId: more-info) via the external bus when the frontend is visible and supported
  • Add an in-place reload option (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

Comment thread app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt Outdated
…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>
@markfrancisonly

markfrancisonly commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

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 TimoPtr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread common/src/main/kotlin/io/homeassistant/companion/android/util/UrlUtil.kt Outdated
Comment thread app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendScreen.kt Outdated
@home-assistant

Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@home-assistant
home-assistant Bot marked this pull request as draft July 13, 2026 11:32
markfrancisonly and others added 2 commits July 19, 2026 14:18
…-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>
@markfrancisonly

Copy link
Copy Markdown
Contributor Author

Thanks — reworked to the Intent approach in b08e2b1 (after merging main, which also drops the legacy WebViewActivity changes):

  • command_webview always builds a LaunchActivity deep link (NavigateTo, or the new ReloadFrontend) with CLEAR_TOP | SINGLE_TOP
  • LaunchActivity now handles onNewIntent: the deep link is handed to the visible frontend's ViewModel to act in place, otherwise it navigates to the frontend route — no more mediators or visibility tracking
  • version gating and the default-dashboard path live in the ViewModel
  • the UrlUtil fix will move to its own PR

@markfrancisonly
markfrancisonly requested a review from TimoPtr July 19, 2026 18:59
markfrancisonly and others added 2 commits July 19, 2026 15:02
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>
@markfrancisonly

markfrancisonly commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

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 lovelace/frontdoor case from #5381) and absolute URLs take a full page load instead of the bus; a navigation waiting for the frontend handshake can no longer land on the wrong server (a server switch cancels it and the shown server is revalidated once the page is ready); and the reload keeps its cache-dropping behavior — rationale in the inline thread. Ready for another look @TimoPtr.

markfrancisonly and others added 2 commits July 19, 2026 16:47
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>
@TimoPtr

TimoPtr commented Jul 20, 2026

Copy link
Copy Markdown
Member

the UrlUtil fix will move to its own PR

You mentioned that UrlUtil will move to its own PR, I don't see any. Is that expected?

@jpelgrom

Copy link
Copy Markdown
Member

@dshokouhi You might remember this, but wasn't opening a new activity and not replacing the history of any open 'main' app here intentional?

@dshokouhi

Copy link
Copy Markdown
Member

@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.

@TimoPtr

TimoPtr commented Jul 21, 2026

Copy link
Copy Markdown
Member

@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?

@dshokouhi

Copy link
Copy Markdown
Member

@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.

@TimoPtr

TimoPtr commented Jul 21, 2026

Copy link
Copy Markdown
Member

@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?

@dshokouhi

Copy link
Copy Markdown
Member

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.

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.

command_webview should not relaunch application

5 participants