From f37e34e9debc7b2e1b110b518b6fbc551b51c693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Sat, 20 Apr 2024 20:09:52 +0200 Subject: [PATCH] Provide interface for external bus outside WebViewActivity (#4338) - Adds an interface to the app to interact with the external bus outside the WebViewActivity. This should make it possible for the frontend to control more native UI of the app. --- .../companion/android/webview/WebView.kt | 3 + .../android/webview/WebViewActivity.kt | 95 ++++++++++--------- .../android/webview/WebViewPresenter.kt | 3 + .../android/webview/WebViewPresenterImpl.kt | 19 ++++ .../webview/externalbus/ExternalBusMessage.kt | 12 +++ .../webview/externalbus/ExternalBusModule.kt | 16 ++++ .../externalbus/ExternalBusRepository.kt | 34 +++++++ .../externalbus/ExternalBusRepositoryImpl.kt | 43 +++++++++ 8 files changed, 179 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusMessage.kt create mode 100644 app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusModule.kt create mode 100644 app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusRepository.kt create mode 100644 app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusRepositoryImpl.kt diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/WebView.kt b/app/src/main/java/io/homeassistant/companion/android/webview/WebView.kt index fc8ada6d6ec..35462056690 100644 --- a/app/src/main/java/io/homeassistant/companion/android/webview/WebView.kt +++ b/app/src/main/java/io/homeassistant/companion/android/webview/WebView.kt @@ -1,6 +1,7 @@ package io.homeassistant.companion.android.webview import android.net.http.SslError +import io.homeassistant.companion.android.webview.externalbus.ExternalBusMessage interface WebView { enum class ErrorType { @@ -16,6 +17,8 @@ interface WebView { fun setExternalAuth(script: String) + fun sendExternalBusMessage(message: ExternalBusMessage) + fun relaunchApp() fun unlockAppIfNeeded() diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt index 545905d4b1b..1969cc93e32 100644 --- a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt @@ -101,6 +101,7 @@ import io.homeassistant.companion.android.util.TLSWebViewClient import io.homeassistant.companion.android.util.isStarted import io.homeassistant.companion.android.websocket.WebsocketManager import io.homeassistant.companion.android.webview.WebView.ErrorType +import io.homeassistant.companion.android.webview.externalbus.ExternalBusMessage import java.util.concurrent.Executors import javax.inject.Inject import javax.inject.Named @@ -155,14 +156,17 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi } } private val writeNfcTag = registerForActivityResult(WriteNfcTag()) { messageId -> - webView.externalBus( - id = messageId, - type = "result", - success = true, - result = emptyMap() - ) { - Log.d(TAG, "NFC Write Complete $it") - } + sendExternalBusMessage( + ExternalBusMessage( + id = messageId, + type = "result", + success = true, + result = emptyMap(), + callback = { + Log.d(TAG, "NFC Write Complete $it") + } + ) + ) } private val showWebFileChooser = registerForActivityResult(ShowWebFileChooser()) { result -> mFilePathCallback?.onReceiveValue(result) @@ -726,23 +730,26 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi val hasNfc = pm.hasSystemFeature(PackageManager.FEATURE_NFC) val canCommissionMatter = presenter.appCanCommissionMatterDevice() val canExportThread = presenter.appCanExportThreadCredentials() - webView.externalBus( - id = JSONObject(message).get("id"), - type = "result", - success = true, - result = JSONObject( - mapOf( - "hasSettingsScreen" to true, - "canWriteTag" to hasNfc, - "hasExoPlayer" to true, - "canCommissionMatter" to canCommissionMatter, - "canImportThreadCredentials" to canExportThread, - "hasAssist" to true - ) + sendExternalBusMessage( + ExternalBusMessage( + id = JSONObject(message).get("id"), + type = "result", + success = true, + result = JSONObject( + mapOf( + "hasSettingsScreen" to true, + "canWriteTag" to hasNfc, + "hasExoPlayer" to true, + "canCommissionMatter" to canCommissionMatter, + "canImportThreadCredentials" to canExportThread, + "hasAssist" to true + ) + ), + callback = { + Log.d(TAG, "Callback $it") + } ) - ) { - Log.d(TAG, "Callback $it") - } + ) // TODO This feature is deprecated and should be removed after 2022.6 getAndSetStatusBarNavigationBarColors() @@ -793,6 +800,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi "exoplayer/resize" -> exoResizeHls(json) "haptic" -> processHaptic(json.getJSONObject("payload").getString("hapticType")) "theme-update" -> getAndSetStatusBarNavigationBarColors() + else -> presenter.onExternalBusMessage(json) } } } @@ -958,14 +966,16 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi exoPlayerView.visibility = View.VISIBLE findViewById(R.id.exo_ha_mute)?.setOnClickListener { exoToggleMute() } } - webView.externalBus( - id = json.get("id"), - type = "result", - success = true, - result = null - ) { - Log.d(TAG, "Callback $it") - } + sendExternalBusMessage( + ExternalBusMessage( + id = json.get("id"), + type = "result", + success = true, + callback = { + Log.d(TAG, "Callback $it") + } + ) + ) } fun exoStopHls() { @@ -1490,28 +1500,21 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi ) } - private fun WebView.externalBus( - id: Any, - type: String, - success: Boolean, - result: Any? = null, - error: Any? = null, - callback: ValueCallback? - ) { + override fun sendExternalBusMessage(message: ExternalBusMessage) { val map = mutableMapOf( - "id" to id, - "type" to type, - "success" to success + "id" to message.id, + "type" to message.type, + "success" to message.success ) - if (result != null) map["result"] = result - if (error != null) map["error"] = error + message.result?.let { map["result"] = it } + message.error?.let { map["error"] = it } val json = JSONObject(map.toMap()) val script = "externalBus($json);" Log.d(TAG, script) - this.evaluateJavascript(script, callback) + webView.evaluateJavascript(script, message.callback) } private fun downloadFile(url: String, contentDisposition: String, mimetype: String) { diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenter.kt b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenter.kt index 21baaedce40..82a9035ae34 100644 --- a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenter.kt +++ b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenter.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.IntentSender import androidx.activity.result.ActivityResult import kotlinx.coroutines.flow.Flow +import org.json.JSONObject interface WebViewPresenter { @@ -41,6 +42,8 @@ interface WebViewPresenter { fun sessionTimeOut(): Int + fun onExternalBusMessage(message: JSONObject) + fun onStart(context: Context) fun onFinish() diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt index 3bb397439cd..161d6a34442 100644 --- a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt @@ -17,6 +17,7 @@ import io.homeassistant.companion.android.matter.MatterManager import io.homeassistant.companion.android.thread.ThreadManager import io.homeassistant.companion.android.util.UrlUtil import io.homeassistant.companion.android.util.UrlUtil.baseIsEqual +import io.homeassistant.companion.android.webview.externalbus.ExternalBusRepository import java.net.SocketTimeoutException import java.net.URL import java.util.regex.Matcher @@ -35,10 +36,12 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.json.JSONObject class WebViewPresenterImpl @Inject constructor( @ActivityContext context: Context, private val serverManager: ServerManager, + private val externalBusRepository: ExternalBusRepository, private val prefsRepository: PrefsRepository, private val matterUseCase: MatterManager, private val threadUseCase: ThreadManager @@ -63,6 +66,16 @@ class WebViewPresenterImpl @Inject constructor( init { updateActiveServer() + + mainScope.launch { + externalBusRepository.getSentFlow().collect { + try { + view.sendExternalBusMessage(it) + } catch (e: Exception) { + Log.w(TAG, "Unable to send message to external bus $it", e) + } + } + } } override fun onViewReady(path: String?) { @@ -285,6 +298,12 @@ class WebViewPresenterImpl @Inject constructor( prefsRepository.isAlwaysShowFirstViewOnAppStartEnabled() } + override fun onExternalBusMessage(message: JSONObject) { + mainScope.launch { + externalBusRepository.received(message) + } + } + override fun sessionTimeOut(): Int = runBlocking { serverManager.getServer(serverId)?.let { serverManager.integrationRepository(serverId).getSessionTimeOut() diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusMessage.kt b/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusMessage.kt new file mode 100644 index 00000000000..741715b3e9b --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusMessage.kt @@ -0,0 +1,12 @@ +package io.homeassistant.companion.android.webview.externalbus + +import android.webkit.ValueCallback + +data class ExternalBusMessage( + val id: Any, + val type: String, + val success: Boolean, + val result: Any? = null, + val error: Any? = null, + val callback: ValueCallback? = null +) diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusModule.kt b/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusModule.kt new file mode 100644 index 00000000000..bb1791e9c5a --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusModule.kt @@ -0,0 +1,16 @@ +package io.homeassistant.companion.android.webview.externalbus + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class ExternalBusModule { + + @Binds + @Singleton + abstract fun externalBusRepository(externalBusRepositoryImpl: ExternalBusRepositoryImpl): ExternalBusRepository +} diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusRepository.kt b/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusRepository.kt new file mode 100644 index 00000000000..12c2f702bae --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusRepository.kt @@ -0,0 +1,34 @@ +package io.homeassistant.companion.android.webview.externalbus + +import kotlinx.coroutines.flow.Flow +import org.json.JSONObject + +/** + * A repository to communicate with the external bus which is provided by the frontend, + * in contexts where there is no 'line of sight' to the webview (usually: other activity). + * + * The [WebViewActivity] or listener should be alive for this to work, and the repository + * does not guarantee that the the receiver will immediately receive the message as the + * system can limit background activity. + */ +interface ExternalBusRepository { + + /** Send a message to the external bus (for native) */ + suspend fun send(message: ExternalBusMessage) + + /** + * Register to receive certain messages from the external bus (for native) + * @param types List of which message `type`s should be received + * @return Flow with received messages for the specified types + */ + fun receive(types: List): Flow + + /** Send a message from the external bus to registered receivers (for webview) */ + suspend fun received(message: JSONObject) + + /** + * @return Flow with [ExternalBusMessage]s that should be sent on the external + * bus (for webview) + */ + fun getSentFlow(): Flow +} diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusRepositoryImpl.kt b/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusRepositoryImpl.kt new file mode 100644 index 00000000000..67ff477b14d --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/webview/externalbus/ExternalBusRepositoryImpl.kt @@ -0,0 +1,43 @@ +package io.homeassistant.companion.android.webview.externalbus + +import android.util.Log +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import org.json.JSONObject + +class ExternalBusRepositoryImpl @Inject constructor() : ExternalBusRepository { + + companion object { + private const val TAG = "ExternalBusRepo" + } + + private val externalBusFlow = MutableSharedFlow( + // Don't suspend if the WebView is temporarily unavailable + extraBufferCapacity = 100 + ) + private val receiverFlows = mutableMapOf, MutableSharedFlow>() + + override suspend fun send(message: ExternalBusMessage) { + externalBusFlow.emit(message) + } + + override fun receive(types: List): Flow { + val flow = receiverFlows[types] ?: MutableSharedFlow() + receiverFlows[types] = flow + return flow.asSharedFlow() + } + + override suspend fun received(message: JSONObject) { + if (!message.has("type")) return + val type = message.getString("type") + val receivers = receiverFlows.filter { it.key.contains(type) } + Log.d(TAG, "Sending message of type $type to ${receivers.size} receiver(s)") + receivers.forEach { (_, flow) -> + flow.emit(message) + } + } + + override fun getSentFlow(): Flow = externalBusFlow.asSharedFlow() +}