-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Navigate and reload the frontend in place for the webview command #7163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0d3e588
6ee7422
25b8af7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,8 @@ import io.homeassistant.companion.android.util.HAWebChromeClient | |
| import io.homeassistant.companion.android.util.HAWebViewClient | ||
| import io.homeassistant.companion.android.util.HAWebViewClientFactory | ||
| import io.homeassistant.companion.android.util.LifecycleHandler | ||
| import io.homeassistant.companion.android.util.ReloadRequestMediator | ||
| import io.homeassistant.companion.android.util.WebViewNavigationMediator | ||
| import io.homeassistant.companion.android.util.hasSameOrigin | ||
| import javax.inject.Inject | ||
| import kotlin.coroutines.cancellation.CancellationException | ||
|
|
@@ -74,6 +76,7 @@ import kotlinx.coroutines.flow.asStateFlow | |
| import kotlinx.coroutines.flow.collectLatest | ||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||
| import kotlinx.coroutines.flow.emitAll | ||
| import kotlinx.coroutines.flow.first | ||
| import kotlinx.coroutines.flow.flow | ||
| import kotlinx.coroutines.flow.map | ||
| import kotlinx.coroutines.flow.merge | ||
|
|
@@ -129,6 +132,8 @@ internal class FrontendViewModel @VisibleForTesting constructor( | |
| private val improvHandler: FrontendImprovHandler, | ||
| private val barcodeScannerHandler: FrontendBarcodeScannerHandler, | ||
| private val matterThreadHandler: FrontendMatterThreadHandler, | ||
| private val reloadRequestMediator: ReloadRequestMediator, | ||
| private val webViewNavigationMediator: WebViewNavigationMediator, | ||
| @NamedKeyChain private val keyChainRepository: KeyChainRepository, | ||
| ) : ViewModel(), | ||
| FrontendConnectionErrorStateProvider { | ||
|
|
@@ -154,6 +159,8 @@ internal class FrontendViewModel @VisibleForTesting constructor( | |
| improvHandler: FrontendImprovHandler, | ||
| barcodeScannerHandler: FrontendBarcodeScannerHandler, | ||
| matterThreadHandler: FrontendMatterThreadHandler, | ||
| reloadRequestMediator: ReloadRequestMediator, | ||
| webViewNavigationMediator: WebViewNavigationMediator, | ||
| @NamedKeyChain keyChainRepository: KeyChainRepository, | ||
| ) : this( | ||
| initialServerId = savedStateHandle.toRoute<FrontendRoute>().serverId, | ||
|
|
@@ -176,6 +183,8 @@ internal class FrontendViewModel @VisibleForTesting constructor( | |
| improvHandler = improvHandler, | ||
| barcodeScannerHandler = barcodeScannerHandler, | ||
| matterThreadHandler = matterThreadHandler, | ||
| reloadRequestMediator = reloadRequestMediator, | ||
| webViewNavigationMediator = webViewNavigationMediator, | ||
| keyChainRepository = keyChainRepository, | ||
| ) | ||
|
|
||
|
|
@@ -306,6 +315,8 @@ internal class FrontendViewModel @VisibleForTesting constructor( | |
| */ | ||
| private var pendingMoreInfoEntityId: String? = null | ||
|
|
||
| private var isFrontendVisible = false | ||
|
|
||
| /** | ||
| * The user's "Autoplay video" preference. | ||
| * | ||
|
|
@@ -358,6 +369,35 @@ internal class FrontendViewModel @VisibleForTesting constructor( | |
| } | ||
| } | ||
|
|
||
| viewModelScope.launch { | ||
| reloadRequestMediator.eventFlow.collect { | ||
| // Dropping the cache while the page is still loading can wedge the load | ||
| _webViewActions.emit( | ||
| if (_viewState.value is FrontendViewState.Content) { | ||
| WebViewAction.HardReload() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why doing a hard reload? |
||
| } else { | ||
| WebViewAction.Reload() | ||
| }, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| viewModelScope.launch { | ||
| // Requests are only made for servers supporting navigate, no version check is needed. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are assuming that the mediator is always going to gate the version, but it might change in the future. |
||
| // 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 -> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| _viewState.first { it is FrontendViewState.Content } | ||
| when (target) { | ||
| is FrontendTarget.EntityMoreInfo -> _webViewActions.emit( | ||
| WebViewAction.OpenMoreInfo(target.entityId), | ||
| ) | ||
| is FrontendTarget.Path -> externalBusRepository.send(NavigateToMessage(path = target.path)) | ||
| FrontendTarget.Default -> externalBusRepository.send(NavigateToMessage(path = "/")) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not using |
||
| } | ||
| } | ||
| } | ||
|
|
||
| viewModelScope.launch { | ||
| frontendBusObserver.messageResults().collect { result -> | ||
| handleMessageResult(result) | ||
|
|
@@ -543,6 +583,9 @@ internal class FrontendViewModel @VisibleForTesting constructor( | |
| _viewState.update { | ||
| FrontendViewState.LoadServer(serverId = serverId) | ||
| } | ||
| if (isFrontendVisible) { | ||
| webViewNavigationMediator.setVisibleServer(serverId) | ||
| } | ||
| loadServer() | ||
| } | ||
|
|
||
|
|
@@ -682,6 +725,12 @@ internal class FrontendViewModel @VisibleForTesting constructor( | |
| } | ||
| } | ||
|
|
||
| /** Publishes the shown server while the frontend is visible so the webview command can act on it in place. */ | ||
| fun setFrontendVisible(visible: Boolean) { | ||
| isFrontendVisible = visible | ||
| webViewNavigationMediator.setVisibleServer(_viewState.value.serverId.takeIf { visible }) | ||
| } | ||
|
|
||
| /** | ||
| * Clears the WebView history and navigates the frontend to the server's default dashboard. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package io.homeassistant.companion.android.util | ||
|
|
||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
| import kotlinx.coroutines.flow.MutableSharedFlow | ||
| import kotlinx.coroutines.flow.asSharedFlow | ||
|
|
||
| /** | ||
| * Carries reload requests from the server commands to the frontend. Requests emitted while the | ||
| * frontend is not open are dropped since there is nothing to reload. | ||
| */ | ||
| @Singleton | ||
| class ReloadRequestMediator @Inject constructor() { | ||
|
|
||
| private val _eventFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1) | ||
| val eventFlow = _eventFlow.asSharedFlow() | ||
|
|
||
| fun emitReloadRequestEvent() { | ||
| _eventFlow.tryEmit(Unit) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package io.homeassistant.companion.android.util | ||
|
|
||
| import io.homeassistant.companion.android.common.data.servers.ServerManager | ||
| import io.homeassistant.companion.android.frontend.navigation.FrontendTarget | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
| import kotlinx.coroutines.flow.MutableSharedFlow | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.asSharedFlow | ||
| import kotlinx.coroutines.flow.asStateFlow | ||
|
|
||
| /** | ||
| * Carries webview navigation requests from the server commands to the visible frontend. | ||
| */ | ||
| @Singleton | ||
| class WebViewNavigationMediator @Inject constructor() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you really need to know if the frontend is visible?? |
||
|
|
||
| private val _visibleServerId = MutableStateFlow<Int?>(null) | ||
|
|
||
| /** The server shown by the visible frontend, or `null` when no frontend is visible. */ | ||
| val visibleServerId: StateFlow<Int?> = _visibleServerId.asStateFlow() | ||
|
|
||
| private val _navigationRequests = MutableSharedFlow<FrontendTarget>(extraBufferCapacity = 1) | ||
| val navigationRequests = _navigationRequests.asSharedFlow() | ||
|
|
||
| /** [ServerManager.SERVER_ID_ACTIVE] is normalized to `null` since it does not identify a server. */ | ||
| fun setVisibleServer(serverId: Int?) { | ||
| _visibleServerId.value = serverId?.takeUnless { it == ServerManager.SERVER_ID_ACTIVE } | ||
| } | ||
|
|
||
| fun requestNavigation(target: FrontendTarget) { | ||
| _navigationRequests.tryEmit(target) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Look at the pattern in this file we already have a Effect high level composable where to put such Effect.