Skip to content
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

Provide interface for external bus outside WebViewActivity #4338

Merged
merged 1 commit into from
Apr 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,6 +17,8 @@ interface WebView {

fun setExternalAuth(script: String)

fun sendExternalBusMessage(message: ExternalBusMessage)

fun relaunchApp()

fun unlockAppIfNeeded()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String, String>()
) {
Log.d(TAG, "NFC Write Complete $it")
}
sendExternalBusMessage(
ExternalBusMessage(
id = messageId,
type = "result",
success = true,
result = emptyMap<String, String>(),
callback = {
Log.d(TAG, "NFC Write Complete $it")
}
)
)
}
private val showWebFileChooser = registerForActivityResult(ShowWebFileChooser()) { result ->
mFilePathCallback?.onReceiveValue(result)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -958,14 +966,16 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
exoPlayerView.visibility = View.VISIBLE
findViewById<ImageButton>(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() {
Expand Down Expand Up @@ -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<String>?
) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -41,6 +42,8 @@ interface WebViewPresenter {

fun sessionTimeOut(): Int

fun onExternalBusMessage(message: JSONObject)

fun onStart(context: Context)

fun onFinish()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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?) {
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>? = null
)
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<String>): Flow<JSONObject>

/** 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<ExternalBusMessage>
}
Original file line number Diff line number Diff line change
@@ -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<ExternalBusMessage>(
// Don't suspend if the WebView is temporarily unavailable
extraBufferCapacity = 100
)
private val receiverFlows = mutableMapOf<List<String>, MutableSharedFlow<JSONObject>>()

override suspend fun send(message: ExternalBusMessage) {
externalBusFlow.emit(message)
}

override fun receive(types: List<String>): Flow<JSONObject> {
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<ExternalBusMessage> = externalBusFlow.asSharedFlow()
}