Refactor WebViewActivity path handling#6447
Refactor WebViewActivity path handling#6447Gifford47 wants to merge 16 commits intohome-assistant:mainfrom
Conversation
|
@TimoPtr can you please have a look on it? you have additional webview PRs ... maybe there's conflict between the PRs ... |
TimoPtr
left a comment
There was a problem hiding this comment.
Thanks for your work, did you test it in multiple conditions? Like changing quickly the URL multiple times?
Also yes I'm currently working on making a new version of this WebViewActivity, it is going to take some time but you can already look at how it looks #6386
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt
Outdated
Show resolved
Hide resolved
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt
Outdated
Show resolved
Hide resolved
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt
Outdated
Show resolved
Hide resolved
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt
Outdated
Show resolved
Hide resolved
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt
Show resolved
Hide resolved
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt
Outdated
Show resolved
Hide resolved
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt
Show resolved
Hide resolved
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt
Outdated
Show resolved
Hide resolved
|
Also check what is happening when having arguments in the url like some filtered on the history page. |
There was a problem hiding this comment.
Hello @Gifford47,
When attempting to inspect the commits of your pull request for CLA signature status among all authors we encountered commit(s) which were not linked to a GitHub account, thus not allowing us to determine their status(es).
The commits that are missing a linked GitHub account are the following:
13fbe0d9cfce5407b151478980805bd257773cba- This commit has something that looks like an email address (let66589@gmail.com). Maybe try linking that to GitHub?.
Unfortunately, we are unable to accept this pull request until this situation is corrected.
Here are your options:
-
If you had an email address set for the commit that simply wasn't linked to your GitHub account you can link that email now and it will retroactively apply to your commits. The simplest way to do this is to click the link to one of the above commits and look for a blue question mark in a blue circle in the top left. Hovering over that bubble will show you what email address you used. Clicking on that button will take you to your email address settings on GitHub. Just add the email address on that page and you're all set. GitHub has more information about this option in their help center.
-
If you didn't use an email address at all, it was an invalid email, or it's one you can't link to your GitHub, you will need to change the authorship information of the commit and your global Git settings so this doesn't happen again going forward. GitHub provides some great instructions on how to change your authorship information in their help center.
- If you only made a single commit you should be able to run
(substituting "Author Name" and "
git commit --amend --author="Author Name <email@address.com>"email@address.com" for your actual information) to set the authorship information. - If you made more than one commit and the commit with the missing authorship information is not the most recent one you have two options:
- You can re-create all commits missing authorship information. This is going to be the easiest solution for developers that aren't extremely confident in their Git and command line skills.
- You can use this script that GitHub provides to rewrite history. Please note: this should be used only if you are very confident in your abilities and understand its impacts.
- Whichever method you choose, I will come by to re-check the pull request once you push the fixes to this branch.
- If you only made a single commit you should be able to run
We apologize for this inconvenience, especially since it usually bites new contributors to Home Assistant. We hope you understand the need for us to protect ourselves and the great community we all have built legally. The best thing to come out of this is that you only need to fix this once and it benefits the entire Home Assistant and GitHub community.
Thanks, I look forward to checking this PR again soon! ❤️
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
|
Thanks for the thorough review @TimoPtr! I've addressed your feedback: Duplicate path retrieval: Removed getCurrentWebViewPath() from the Activity — the Presenter already handles this fallback in collectUrlStateChanges(), so the Activity now only passes the intentPath (or null). intent.removeExtra(EXTRA_PATH): Moved before presenter.load() so the extra is consumed immediately. moreInfoEntity assignment: Fixed the behavioral change — removed the unconditional raw assignment. Now only sets moreInfoEntity to the regex-validated entity for older servers (JS dispatch path), and explicitly clears it when using the URL path approach (>= 2025.6). Exception handling in getCurrentWebViewPath(): Removed the broad catch (Exception) — Uri.parse() is lenient and doesn't throw, so the try-catch was unnecessary. Path extraction simplification: Replaced with webView.url?.toUri()?.path?.takeIf { it.length > 1 } as suggested. Comment reference: Updated to use the full GitHub issue URL. Regarding URL arguments (e.g. filters on the history page): Uri.path only returns the path component and excludes query parameters, so filtered views like /history?entity_id=sensor.foo will correctly preserve just /history as the path — the frontend will reload with its default state for that view, which is the expected behavior when switching between internal/external URLs. |
Actually it might be nice to keep the whole URI and just change the host/port imagine you are on It would be nice to only change the host/port |
|
The issue would be that most probably you are going to loose the history and if it is not the case I wonder what happens. Did you try to play with the app to see how it behaves for the history? |
There was a problem hiding this comment.
Hello @Gifford47,
When attempting to inspect the commits of your pull request for CLA signature status among all authors we encountered commit(s) which were not linked to a GitHub account, thus not allowing us to determine their status(es).
The commits that are missing a linked GitHub account are the following:
bb236239032f77af1c6d3175f0bdbd8d5c4818e0- This commit has something that looks like an email address (let66589@gmail.com). Maybe try linking that to GitHub?.
Unfortunately, we are unable to accept this pull request until this situation is corrected.
Here are your options:
-
If you had an email address set for the commit that simply wasn't linked to your GitHub account you can link that email now and it will retroactively apply to your commits. The simplest way to do this is to click the link to one of the above commits and look for a blue question mark in a blue circle in the top left. Hovering over that bubble will show you what email address you used. Clicking on that button will take you to your email address settings on GitHub. Just add the email address on that page and you're all set. GitHub has more information about this option in their help center.
-
If you didn't use an email address at all, it was an invalid email, or it's one you can't link to your GitHub, you will need to change the authorship information of the commit and your global Git settings so this doesn't happen again going forward. GitHub provides some great instructions on how to change your authorship information in their help center.
- If you only made a single commit you should be able to run
(substituting "Author Name" and "
git commit --amend --author="Author Name <email@address.com>"email@address.com" for your actual information) to set the authorship information. - If you made more than one commit and the commit with the missing authorship information is not the most recent one you have two options:
- You can re-create all commits missing authorship information. This is going to be the easiest solution for developers that aren't extremely confident in their Git and command line skills.
- You can use this script that GitHub provides to rewrite history. Please note: this should be used only if you are very confident in your abilities and understand its impacts.
- Whichever method you choose, I will come by to re-check the pull request once you push the fixes to this branch.
- If you only made a single commit you should be able to run
We apologize for this inconvenience, especially since it usually bites new contributors to Home Assistant. We hope you understand the need for us to protect ourselves and the great community we all have built legally. The best thing to come out of this is that you only need to fix this once and it benefits the entire Home Assistant and GitHub community.
Thanks, I look forward to checking this PR again soon! ❤️
|
Changes
Added getCurrentWebViewRelativeUrl() which extracts the full relative URL (path + query parameters + fragment) from the current WebView URL, stripping the external_auth parameter since the presenter re-adds it on every load.
Added lastBaseUrl tracking in collectUrlStateChanges to detect when the base URL changes (internal ↔ external).
After history is cleared, rapid urlFlow emissions can create stale history entries from the old connection. The back button handler now validates the origin of the previous history entry using WebBackForwardList before navigating back. Tested: WiFi → mobile data switch on Lovelace tabs: URL switches correctly, page preserved |
|
@TimoPtr all tests are successfully done. Do you have any hints? |
Instead of only preserving the path when the connection switches between internal and external URLs, preserve the complete relative URL including query parameters and fragment. This ensures filtered views (e.g. history page with date ranges) survive URL switches seamlessly. - Rename getCurrentWebViewPath() to getCurrentWebViewRelativeUrl() - Extract path + query params + fragment from the current WebView URL - Strip 'external_auth' param to avoid duplication (presenter re-adds it) - Update presenter to use the new method with descriptive variable names Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the base URL changes (e.g. switching from internal Wi-Fi to external mobile data), old URLs in the back stack become unreachable on the new network. Pressing back would attempt to load those old URLs, causing timeouts and error messages. Track the last base URL in collectUrlStateChanges and set isNewServer=true when a change is detected, which triggers keepHistory=false and clears the navigation history after loading the new URL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After switching between internal and external URLs, stale history entries from the old connection may remain in the WebView. This change validates that the previous history entry has the same origin as the current URL before navigating back. If the origin differs, it navigates to the base URL instead of attempting to load an unreachable page. Also keeps the back callback enabled when on a non-root path so pressing back navigates to root before exiting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The WebView interface gained a new getCurrentWebViewRelativeUrl() method but the FakeWebViewContext test wrapper was not updated, causing a compilation failure in unit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
A relaxed MockK mock returns "" instead of null for String? return types. This caused UrlUtil.handle to normalize the URL with a trailing slash, breaking the exact string assertion in the "previous load in progress" test. Explicitly mock getCurrentWebViewRelativeUrl to return null, matching the real behavior when no WebView page is loaded. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Guard back navigation against about:blank and other non-HTTP history entries that may appear before the first real page has loaded. - Use Uri.Builder in getCurrentWebViewRelativeUrl instead of manual string concatenation for safer URL construction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract URL relative-path logic into a reusable Uri.toRelativeUrl() extension in UrlUtil.kt, using queryParameterNames API instead of string splitting (as suggested by TimoPtr) - Add KDoc documentation to the new extension - Simplify getCurrentWebViewRelativeUrl() to a single-line delegation - Add comments explaining the callback disable/enable pattern and the about:blank edge case in back navigation - Add 10 unit tests for toRelativeUrl() in UriExtensionsTest.kt
Restore original placement to keep git history clean, as requested by TimoPtr.
…tyle - Move hideSystemUI/showSystemUI block back before path processing to preserve original code order and keep git history clean - Refactor toRelativeUrl() query parameter loop to functional style using filterNot/flatMap/forEach as suggested by TimoPtr
|
I'm using this logic now for quite a while and everything works as expected. Any suggestions @TimoPtr ? |
|
I need to play with it to see how it behaves, I didn't have time to do this yet. @jpelgrom I think this is also ready for you to look at. I'm mostly curious about the back behavior while changing URLs. We will also need to port this change to the |
jpelgrom
left a comment
There was a problem hiding this comment.
The path is preserved when switching servers, which should not happen as the path may not exist on the other server (resulting in a broken page) and it leaks information about the initial server to the other server.
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt
Outdated
Show resolved
Hide resolved
| // Keep the callback enabled when there's history OR when the current | ||
| // URL has a non-root path (so pressing back navigates to root first). | ||
| onBackPressed.isEnabled = canGoBack() || | ||
| url?.toUri()?.hasNonRootPath() == true |
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt
Show resolved
Hide resolved
common/src/main/kotlin/io/homeassistant/companion/android/util/UrlUtil.kt
Outdated
Show resolved
Hide resolved
common/src/test/kotlin/io/homeassistant/companion/android/util/UriExtensionsTest.kt
Outdated
Show resolved
Hide resolved
|
Thanks for the thorough review @jpelgrom, all great catches! Path leaking on server switch — I'll guard the getCurrentWebViewRelativeUrl() fallback so it only applies on internal/external URL changes (baseUrlChanged), not on actual server switches where isNewServer is true. OnBackPressedCallback(true) — You're right, losing predictive back animations isn't ideal. I'll extract a canHandleBack() function that returns canGoBack() || hasNonRootPath() and use that for the initial enabled state + the doUpdateVisitedHistory update. That way we keep the adaptive behavior. doUpdateVisitedHistory — This is needed so that pressing back navigates to root first even when there's no WebView history. The canHandleBack() extraction should make this clearer. entityId comment — Will restore the original comment with the core.py link. KDoc — Agreed, I'll make it more generic since it's a util function. Test name — You're right, the name is misleading. Will rename to something like toRelativeUrl strips excluded params leaving path only. |
- Don't preserve path on server switch to avoid leaking info and broken pages; only preserve on internal/external URL changes - Restore OnBackPressedCallback(webView.canGoBack()) to keep predictive back animations on Android 14+ - Restore entityId comment with core.py link - Make toRelativeUrl KDoc more generic (it's a util function) - Fix misleading test name that suggested null return
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt
Fixed
Show fixed
Hide fixed
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt
Fixed
Show fixed
Hide fixed
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt
Outdated
Show resolved
Hide resolved
- Move cross-origin check + navigate-to-root logic into WebViewBackNavigation.kt so it can be reused by both WebViewActivity and FrontendScreen. - Rename isNewServer → clearHistory in handleUrlState/loadUrl for clarity (requested by TimoPtr). - Fix ktlint indentation in WebViewPresenterImpl.kt:584.
Resolve import conflict: keep BackAction/resolveBackAction imports alongside new BLANK_URL import from upstream.
| @@ -0,0 +1,74 @@ | |||
| package io.homeassistant.companion.android.util.compose.webview | |||
Check failure
Code scanning / ktlint
File 'WebViewBackNavigation.kt' contains a single class and possibly also extension functions for that class and should be named same after that class 'BackAction.kt' Error
| @@ -0,0 +1,74 @@ | |||
| package io.homeassistant.companion.android.util.compose.webview | |||
Check failure
Code scanning / ktlint
File 'WebViewBackNavigation.kt' contains a single class and possibly also extension functions for that class and should be named same after that class 'BackAction.kt' Error
| * returns [BackAction.GoBack] so the user can navigate back normally. | ||
| * 2. If there is no valid back history (empty, cross-origin, or non-HTTP entries) | ||
| * and the current URL has a non-root path, returns [BackAction.NavigateToRoot] | ||
| * so the user is taken to the home page first. |
There was a problem hiding this comment.
I'm lost ... What you want is not to go back to root no? it is to preserve the path and param no?
I think it's time to drop your AI agent and start to manually look at the logic.
There was a problem hiding this comment.
Ok got it ... I was lost after looking at this way too many time already. The preserve path and param is only when changing URL not when going back.
That might be something we could consider actually too. (In another PR)
I still think that to get this final step you should drop your AI agent and do it manually it's quite sensitive.
| sealed interface BackAction { | ||
| /** Navigate back in the WebView history. */ | ||
| data object GoBack : BackAction | ||
|
|
||
| /** Clear history and navigate to the root URL of the current server. */ | ||
| data class NavigateToRoot(val rootUrl: Uri) : BackAction | ||
|
|
||
| /** No more back navigation possible — exit the screen. */ | ||
| data object Exit : BackAction | ||
| } |
There was a problem hiding this comment.
I'm not a big fan of the Exit action it seems too precise, and set the action for the screen itself that could decide to do something else. What do you think about None that let the caller decide what to do with it.
| val previousUrl = Uri.parse( | ||
| backForwardList.getItemAtIndex(currentIndex - 1).url, | ||
| ) |
There was a problem hiding this comment.
| val previousUrl = Uri.parse( | |
| backForwardList.getItemAtIndex(currentIndex - 1).url, | |
| ) | |
| val previousUrl = backForwardList.getItemAtIndex(currentIndex - 1).url.toUri() |
| * not necessarily [WebView.getUrl] which can be `about:blank` during loads) | ||
| * @return the [BackAction] that the caller should execute | ||
| */ | ||
| fun resolveBackAction(webView: WebView, loadedUrl: Uri?): BackAction { |
There was a problem hiding this comment.
What about not giving the webView to this function directly, so no one actually call webView.goBack() from this function since it is theoretically possible. Something like
fun resolveBackAction(webView: WebView, loadedUrl: Uri?): BackAction {
val previousUrl = if (webView.canGoBack()) {
val backForwardList = webView.copyBackForwardList()
backForwardList.currentIndex
.takeIf { it > 0 }
?.let { backForwardList.getItemAtIndex(it - 1).url }
?.toUri()
} else {
null
}
return resolveBackAction(previousUrl, loadedUrl)
}
@VisibleForTesting
internal fun resolveBackAction(previousUrl: Uri?, loadedUrl: Uri?): BackAction {
if (previousUrl != null) {This way the logic is in a pure function easy to test even without mocking the WebView.
| clearHistory = true | ||
| loadedUrl = action.rootUrl | ||
| webView.loadUrl(action.rootUrl.toString()) |
There was a problem hiding this comment.
Any reason to not call load of the presenter ? I think it is quite important to go through the presenter logic when we want to load a URL. I have no idea of the implication just thinking, that might avoid having to set the loadedUrl and clearHistory here.
| // Keep the callback enabled when there's history OR when the current | ||
| // URL has a non-root path (so pressing back navigates to root first). | ||
| onBackPressed.isEnabled = canGoBack() || | ||
| url?.toUri()?.hasNonRootPath() == true |
| view.loadUrl( | ||
| url = urlWithAuth, | ||
| keepHistory = !isNewServer, | ||
| keepHistory = !clearHistory, |
There was a problem hiding this comment.
I made a mistake in my last comment, we could use keepHistory just to avoid having to inverse the logic.
| } | ||
|
|
||
| @Test | ||
| fun `resolveBackAction returns Exit when no history and root URL`() { |
There was a problem hiding this comment.
Update the titles to fit the other test in the project
| } | ||
|
|
||
| @Test | ||
| fun `resolveBackAction returns Exit when on root path`() { |
There was a problem hiding this comment.
Same as funresolveBackAction returns Exit when no history and root URL() { no?
Summary
This PR refactors how paths are handled in the WebView to ensure navigation keeps the current path and exposes it reliably.
Key Changes
WebViewPresenterImpl.kt,WebViewActivity.kt,WebView.kt.getCurrentPath()→ retrieves the active WebView path.Why
Impact
getCurrentPath()can be used wherever current WebView state is needed.Checklist
Screenshots
Link to pull request in documentation repositories
User Documentation: home-assistant/companion.home-assistant#
Developer Documentation: home-assistant/developers.home-assistant#
Any other notes
See issue #4983