From b1f463e54524d1031deab93d6c308d0850b4c569 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Thu, 6 Apr 2023 09:07:45 -0400 Subject: [PATCH] Allow Strada to maintain control of Bridge instances. Apps can initialize the bridge with a WebView instance through Bridge.initialize(webView). --- .../main/kotlin/dev/hotwire/strada/Bridge.kt | 57 ++++++++++++++----- .../dev/hotwire/strada/BridgeDelegate.kt | 16 ++++-- .../dev/hotwire/strada/BridgeDelegateTest.kt | 19 ++++--- 3 files changed, 65 insertions(+), 27 deletions(-) diff --git a/strada/src/main/kotlin/dev/hotwire/strada/Bridge.kt b/strada/src/main/kotlin/dev/hotwire/strada/Bridge.kt index 1142185..aeec0ec 100644 --- a/strada/src/main/kotlin/dev/hotwire/strada/Bridge.kt +++ b/strada/src/main/kotlin/dev/hotwire/strada/Bridge.kt @@ -2,66 +2,74 @@ package dev.hotwire.strada import android.webkit.JavascriptInterface import android.webkit.WebView +import androidx.annotation.VisibleForTesting import kotlinx.serialization.json.JsonElement +import java.lang.ref.WeakReference // These need to match whatever is set in strada.js private const val bridgeGlobal = "window.nativeBridge" private const val bridgeJavascriptInterface = "Strada" @Suppress("unused") -class Bridge(private val webView: WebView) { - internal var repository = Repository() +class Bridge internal constructor(webView: WebView) { private var componentsAreRegistered: Boolean = false + private val webViewRef: WeakReference - var delegate: BridgeDelegate<*>? = null + internal val webView: WebView? get() = webViewRef.get() + internal var repository = Repository() + internal var delegate: BridgeDelegate<*>? = null init { + // Use a weak reference in case the WebView is no longer being + // used by the app, such as when the render process is gone. + webViewRef = WeakReference(webView) + // The JavascriptInterface must be added before the page is loaded webView.addJavascriptInterface(this, bridgeJavascriptInterface) } - fun register(component: String) { + internal fun register(component: String) { logEvent("bridgeWillRegisterComponent", component) val javascript = generateJavaScript("register", component.toJsonElement()) evaluate(javascript) } - fun register(components: List) { + internal fun register(components: List) { logEvent("bridgeWillRegisterComponents", components.joinToString()) val javascript = generateJavaScript("register", components.toJsonElement()) evaluate(javascript) } - fun unregister(component: String) { + internal fun unregister(component: String) { logEvent("bridgeWillUnregisterComponent", component) val javascript = generateJavaScript("unregister", component.toJsonElement()) evaluate(javascript) } - fun send(message: Message) { + internal fun send(message: Message) { logMessage("bridgeWillSendMessage", message) val internalMessage = InternalMessage.fromMessage(message) val javascript = generateJavaScript("send", internalMessage.toJson().toJsonElement()) evaluate(javascript) } - fun load() { + internal fun load() { logEvent("bridgeWillLoad") evaluate(userScript()) } - fun reset() { + internal fun reset() { logEvent("bridgeDidReset") componentsAreRegistered = false } - fun isReady(): Boolean { + internal fun isReady(): Boolean { return componentsAreRegistered } @JavascriptInterface fun bridgeDidInitialize() { - logEvent("bridgeDidInitialize") + logEvent("bridgeDidInitialize", "success") runOnUiThread { delegate?.bridgeDidInitialize() } @@ -69,7 +77,7 @@ class Bridge(private val webView: WebView) { @JavascriptInterface fun bridgeDidUpdateSupportedComponents() { - logEvent("bridgeDidUpdateSupportedComponents") + logEvent("bridgeDidUpdateSupportedComponents", "success") componentsAreRegistered = true } @@ -85,12 +93,13 @@ class Bridge(private val webView: WebView) { // Internal internal fun userScript(): String { - return repository.getUserScript(webView.context) + val context = requireNotNull(webView?.context) + return repository.getUserScript(context) } internal fun evaluate(javascript: String) { logEvent("evaluatingJavascript", javascript) - webView.evaluateJavascript(javascript) {} + webView?.evaluateJavascript(javascript) {} } internal fun generateJavaScript(bridgeFunction: String, vararg arguments: JsonElement): String { @@ -106,4 +115,24 @@ class Bridge(private val webView: WebView) { internal fun sanitizeFunctionName(name: String): String { return name.removeSuffix("()") } + + companion object { + private val instances = mutableListOf() + + fun initialize(webView: WebView) { + if (getBridgeFor(webView) == null) { + initialize(Bridge(webView)) + } + } + + @VisibleForTesting + internal fun initialize(bridge: Bridge) { + instances.add(bridge) + instances.removeIf { it.webView == null } + } + + internal fun getBridgeFor(webView: WebView): Bridge? { + return instances.firstOrNull { it.webView == webView } + } + } } diff --git a/strada/src/main/kotlin/dev/hotwire/strada/BridgeDelegate.kt b/strada/src/main/kotlin/dev/hotwire/strada/BridgeDelegate.kt index 0170375..40c0c18 100644 --- a/strada/src/main/kotlin/dev/hotwire/strada/BridgeDelegate.kt +++ b/strada/src/main/kotlin/dev/hotwire/strada/BridgeDelegate.kt @@ -1,5 +1,6 @@ package dev.hotwire.strada +import android.webkit.WebView import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -30,12 +31,17 @@ class BridgeDelegate( bridge?.reset() } - fun onWebViewAttached(bridge: Bridge?) { - this.bridge = bridge - this.bridge?.delegate = this + fun onWebViewAttached(webView: WebView) { + bridge = Bridge.getBridgeFor(webView)?.apply { + delegate = this@BridgeDelegate + } - if (shouldReloadBridge()) { - loadBridgeInWebView() + if (bridge != null) { + if (shouldReloadBridge()) { + loadBridgeInWebView() + } + } else { + logEvent("bridgeNotInitializedForWebView", destination.destinationLocation()) } } diff --git a/strada/src/test/kotlin/dev/hotwire/strada/BridgeDelegateTest.kt b/strada/src/test/kotlin/dev/hotwire/strada/BridgeDelegateTest.kt index 0b8c7f1..8dc8add 100644 --- a/strada/src/test/kotlin/dev/hotwire/strada/BridgeDelegateTest.kt +++ b/strada/src/test/kotlin/dev/hotwire/strada/BridgeDelegateTest.kt @@ -1,5 +1,7 @@ package dev.hotwire.strada +import android.webkit.WebView +import androidx.core.view.get import androidx.lifecycle.testing.TestLifecycleOwner import com.nhaarman.mockito_kotlin.* import org.junit.Assert.assertEquals @@ -11,6 +13,7 @@ import org.mockito.Mockito.verify class BridgeDelegateTest { private lateinit var delegate: BridgeDelegate private val bridge: Bridge = mock() + private val webView: WebView = mock() private val factories = listOf( BridgeComponentFactory("one", ::OneBridgeComponent), @@ -19,29 +22,30 @@ class BridgeDelegateTest { @Before fun setup() { + whenever(bridge.webView).thenReturn(webView) + Bridge.initialize(bridge) + delegate = BridgeDelegate( destination = AppBridgeDestination(), componentFactories = factories ) + delegate.bridge = bridge } @Test fun loadBridgeInWebView() { - delegate.onWebViewAttached(bridge) delegate.loadBridgeInWebView() - verify(bridge, times(2)).load() + verify(bridge).load() } @Test fun resetBridge() { - delegate.onWebViewAttached(bridge) delegate.resetBridge() verify(bridge).reset() } @Test fun bridgeDidInitialize() { - delegate.onWebViewAttached(bridge) delegate.bridgeDidInitialize() verify(bridge).register(eq(listOf("one", "two"))) } @@ -75,7 +79,7 @@ class BridgeDelegateTest { @Test fun onWebViewAttached() { whenever(bridge.isReady()).thenReturn(false) - delegate.onWebViewAttached(bridge) + delegate.onWebViewAttached(webView) assertEquals(delegate.bridge, bridge) } @@ -83,7 +87,7 @@ class BridgeDelegateTest { @Test fun onWebViewAttachedShouldLoad() { whenever(bridge.isReady()).thenReturn(false) - delegate.onWebViewAttached(bridge) + delegate.onWebViewAttached(webView) verify(bridge).load() } @@ -91,14 +95,13 @@ class BridgeDelegateTest { @Test fun onWebViewAttachedShouldNotLoad() { whenever(bridge.isReady()).thenReturn(true) - delegate.onWebViewAttached(bridge) + delegate.onWebViewAttached(webView) verify(bridge, never()).load() } @Test fun onWebViewDetached() { - delegate.onWebViewAttached(bridge) delegate.onWebViewDetached() assertNull(delegate.bridge?.delegate)