Skip to content
Merged

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import io.homeassistant.companion.android.frontend.error.FrontendConnectionError
import io.homeassistant.companion.android.frontend.externalbus.WebViewScript
import io.homeassistant.companion.android.frontend.externalbus.incoming.HapticType
import io.homeassistant.companion.android.frontend.haptic.HapticFeedbackPerformer
import io.homeassistant.companion.android.frontend.js.FrontendJsBridge
import io.homeassistant.companion.android.frontend.js.FrontendJsCallback
import io.homeassistant.companion.android.frontend.permissions.NotificationPermissionPrompt
import io.homeassistant.companion.android.frontend.permissions.PendingWebViewPermissionRequest
import io.homeassistant.companion.android.frontend.permissions.WebViewPermissionEffect
Expand Down Expand Up @@ -167,6 +169,7 @@ internal fun FrontendScreenContent(
WebViewEffects(
webView = webView,
url = viewState.url,
frontendJsCallback = frontendJsCallback,
scriptsToEvaluate = scriptsToEvaluate,
hapticEvents = hapticEvents,
)
Expand All @@ -183,7 +186,6 @@ internal fun FrontendScreenContent(
onWebViewCreated = { webView = it },
webViewClient = webViewClient,
webChromeClient = webChromeClient,
frontendJsCallback = frontendJsCallback,
contentState = viewState as? FrontendViewState.Content,
onWebViewCreationFailed = onWebViewCreationFailed,
)
Expand Down Expand Up @@ -371,7 +373,6 @@ private fun SafeHAWebView(
onBackClick: () -> Unit,
onWebViewCreated: (WebView) -> Unit,
webViewClient: WebViewClient,
frontendJsCallback: FrontendJsCallback,
contentState: FrontendViewState.Content?,
onWebViewCreationFailed: (Throwable) -> Unit,
webChromeClient: WebChromeClient? = null,
Expand Down Expand Up @@ -411,9 +412,6 @@ private fun SafeHAWebView(
.background(Color.Transparent),
configure = {
onWebViewCreated(this)
// Injecting the javascript interface should happen as early as possible in the process
// even before loading the server URL to not miss any events from the frontend.
frontendJsCallback.attachToWebView(this)
this.webViewClient = webViewClient
webChromeClient?.let { this.webChromeClient = it }
},
Expand Down Expand Up @@ -455,11 +453,13 @@ private fun Color.Overlay(modifier: Modifier = Modifier) {
private fun WebViewEffects(
webView: WebView?,
url: String,
frontendJsCallback: FrontendJsCallback,
scriptsToEvaluate: Flow<WebViewScript>,
hapticEvents: Flow<HapticType>,
) {
if (webView != null) {
LaunchedEffect(webView, url) {
frontendJsCallback.attachToWebView(webView)
Timber.v("Load url ${sensitive(url)}")
webView.loadUrl(url)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import io.homeassistant.companion.android.frontend.error.FrontendConnectionError
import io.homeassistant.companion.android.frontend.error.FrontendConnectionErrorStateProvider
import io.homeassistant.companion.android.frontend.externalbus.WebViewScript
import io.homeassistant.companion.android.frontend.externalbus.incoming.HapticType
import io.homeassistant.companion.android.frontend.handler.FrontendBusObserver
import io.homeassistant.companion.android.frontend.handler.FrontendHandlerEvent
import io.homeassistant.companion.android.frontend.handler.FrontendMessageHandler
import io.homeassistant.companion.android.frontend.js.BridgeState
import io.homeassistant.companion.android.frontend.js.FrontendJsBridgeFactory
import io.homeassistant.companion.android.frontend.js.FrontendJsCallback
import io.homeassistant.companion.android.frontend.navigation.FrontendNavigationEvent
import io.homeassistant.companion.android.frontend.navigation.FrontendRoute
import io.homeassistant.companion.android.frontend.permissions.PermissionManager
Expand Down Expand Up @@ -61,29 +64,32 @@ internal class FrontendViewModel @VisibleForTesting constructor(
initialServerId: Int,
initialPath: String?,
webViewClientFactory: HAWebViewClientFactory,
private val frontendMessageHandler: FrontendMessageHandler,
private val frontendBusObserver: FrontendBusObserver,
private val urlManager: FrontendUrlManager,
private val connectivityCheckRepository: ConnectivityCheckRepository,
private val permissionManager: PermissionManager,
private val frontendJsBridgeFactory: FrontendJsBridgeFactory,
) : ViewModel(),
FrontendConnectionErrorStateProvider {

@Inject
constructor(
savedStateHandle: SavedStateHandle,
webViewClientFactory: HAWebViewClientFactory,
frontendMessageHandler: FrontendMessageHandler,
frontendBusObserver: FrontendBusObserver,
urlManager: FrontendUrlManager,
connectivityCheckRepository: ConnectivityCheckRepository,
permissionManager: PermissionManager,
frontendJsBridgeFactory: FrontendJsBridgeFactory,
) : this(
initialServerId = savedStateHandle.toRoute<FrontendRoute>().serverId,
initialPath = savedStateHandle.toRoute<FrontendRoute>().path,
webViewClientFactory = webViewClientFactory,
frontendMessageHandler = frontendMessageHandler,
frontendBusObserver = frontendBusObserver,
urlManager = urlManager,
connectivityCheckRepository = connectivityCheckRepository,
permissionManager = permissionManager,
frontendJsBridgeFactory = frontendJsBridgeFactory,
)

/**
Expand Down Expand Up @@ -147,23 +153,21 @@ internal class FrontendViewModel @VisibleForTesting constructor(
.stateIn(viewModelScope, SharingStarted.Eagerly, (_viewState.value as? FrontendViewState.Error)?.error)

/** Flow of scripts to be evaluated in the WebView. */
val scriptsToEvaluate: Flow<WebViewScript> = frontendMessageHandler.scriptsToEvaluate()
val scriptsToEvaluate: Flow<WebViewScript> = frontendBusObserver.scriptsToEvaluate()

/**
* JavaScript bridge for communication between the WebView and native code.
*
* Must be attached to the WebView via [FrontendJsBridge.attachToWebView].
* Must be attached to the WebView via [io.homeassistant.companion.android.frontend.js.FrontendJsBridge.attachToWebView].
*/
val frontendJsCallback: FrontendJsCallback = FrontendJsBridge(
handler = frontendMessageHandler,
serverIdProvider = { viewState.value.serverId },
val frontendJsCallback: FrontendJsCallback = frontendJsBridgeFactory.create(
scope = viewModelScope,
stateProvider = { BridgeState(serverId = viewState.value.serverId, url = viewState.value.url) },
)

val webViewClient: HAWebViewClient = webViewClientFactory.create(
currentUrlFlow = urlFlow,
onFrontendError = ::onError,
frontendJsCallback = frontendJsCallback,
onCrash = ::onRetry,
)

Expand Down Expand Up @@ -200,7 +204,7 @@ internal class FrontendViewModel @VisibleForTesting constructor(
}

viewModelScope.launch {
frontendMessageHandler.messageResults().collect { result ->
frontendBusObserver.messageResults().collect { result ->
handleMessageResult(result)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.homeassistant.companion.android.frontend.externalbus.incoming.Incoming
import io.homeassistant.companion.android.frontend.externalbus.outgoing.OutgoingExternalBusMessage
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.json.JsonElement

/**
* Represents a JavaScript script to be evaluated in the WebView.
Expand Down Expand Up @@ -59,7 +60,7 @@ interface FrontendExternalBusRepository {
*
* The message is deserialized and emitted to subscribers of [incomingMessages].
*
* @param messageJson The raw JSON string from the frontend
* @param messageJson The JSON message from the frontend
*/
suspend fun onMessageReceived(messageJson: String)
suspend fun onMessageReceived(messageJson: JsonElement)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.modules.plus
import timber.log.Timber

Expand Down Expand Up @@ -65,22 +67,20 @@ class FrontendExternalBusRepositoryImpl @Inject constructor() : FrontendExternal

override fun incomingMessages(): Flow<IncomingExternalBusMessage> = incomingFlow.asSharedFlow()

override suspend fun onMessageReceived(messageJson: String) {
override suspend fun onMessageReceived(messageJson: JsonElement) {
val message = deserializeMessage(messageJson)
if (message != null) {
incomingFlow.emit(message)
}
}

private fun deserializeMessage(json: String): IncomingExternalBusMessage? {
if (json.isBlank()) return null

private fun deserializeMessage(json: JsonElement): IncomingExternalBusMessage? {
return runCatching {
frontendExternalBusJson.decodeFromString<IncomingExternalBusMessage>(json)
frontendExternalBusJson.decodeFromJsonElement<IncomingExternalBusMessage>(json)
}.onFailure { error ->
Timber.w(
error,
"Failed to deserialize external bus message: ${sensitive(json)}",
"Failed to deserialize external bus message: ${sensitive { json.toString() }}",
)
}.getOrNull()
}
Expand Down
Loading
Loading