From 8e5421357b3a7bb70eaa0561dd321051e1c8761d Mon Sep 17 00:00:00 2001 From: nian1 Date: Sun, 25 Aug 2024 19:46:38 +0800 Subject: [PATCH] feat(WebViewJavascriptBridge): Improve setup and lifecycle management --- .../ding1ding/jsbridge/app/MainActivity.kt | 14 +- .../jsbridge/WebViewJavascriptBridge.kt | 213 ++++++++++++------ 2 files changed, 146 insertions(+), 81 deletions(-) diff --git a/app/src/main/java/com/ding1ding/jsbridge/app/MainActivity.kt b/app/src/main/java/com/ding1ding/jsbridge/app/MainActivity.kt index f9fccda..ac6d801 100644 --- a/app/src/main/java/com/ding1ding/jsbridge/app/MainActivity.kt +++ b/app/src/main/java/com/ding1ding/jsbridge/app/MainActivity.kt @@ -28,7 +28,6 @@ class MainActivity : super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setupWebView() - setupBridge() setupClickListeners() } @@ -56,9 +55,6 @@ class MainActivity : } override fun onDestroy() { - // 01 - bridge.release() - // 02 releaseWebView() Log.d(TAG, "onDestroy") super.onDestroy() @@ -84,14 +80,17 @@ class MainActivity : allowUniversalAccessFromFileURLs = true } webViewClient = createWebViewClient() - loadUrl("file:///android_asset/index.html") } webViewContainer.addView(webView) + + setupWebViewBridge(webView) + + webView.loadUrl("file:///android_asset/index.html") } - private fun setupBridge() { - bridge = WebViewJavascriptBridge(this, webView).apply { + private fun setupWebViewBridge(webView: WebView) { + bridge = WebViewJavascriptBridge.create(this, webView, lifecycle).apply { consolePipe = object : ConsolePipe { override fun post(message: String) { Log.d("[console.log]", message) @@ -112,7 +111,6 @@ class MainActivity : private fun createWebViewClient() = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) { Log.d(TAG, "onPageStarted") - bridge.injectJavascript() } override fun onPageFinished(view: WebView?, url: String?) { diff --git a/library/src/main/java/com/ding1ding/jsbridge/WebViewJavascriptBridge.kt b/library/src/main/java/com/ding1ding/jsbridge/WebViewJavascriptBridge.kt index bbb6f83..86fb620 100644 --- a/library/src/main/java/com/ding1ding/jsbridge/WebViewJavascriptBridge.kt +++ b/library/src/main/java/com/ding1ding/jsbridge/WebViewJavascriptBridge.kt @@ -5,133 +5,125 @@ import android.content.Context import android.util.Log import android.webkit.JavascriptInterface import android.webkit.WebView +import android.webkit.WebViewClient import androidx.annotation.MainThread +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -class WebViewJavascriptBridge @JvmOverloads constructor( +class WebViewJavascriptBridge private constructor( private val context: Context, private val webView: WebView, - private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main), -) { - @JvmField +) : DefaultLifecycleObserver { + var consolePipe: ConsolePipe? = null - private val responseCallbacks = mutableMapOf>() - private val messageHandlers = mutableMapOf>() + private val responseCallbacks = ConcurrentHashMap>() + private val messageHandlers = ConcurrentHashMap>() private val uniqueId = AtomicInteger(0) private val bridgeScript by lazy { loadAsset("bridge.js") } private val consoleHookScript by lazy { loadAsset("hookConsole.js") } - private var isInjected = false + private val isInjected = AtomicBoolean(false) + private val isWebViewReady = AtomicBoolean(false) init { setupBridge() - } - - @JvmOverloads - fun reset(clearHandlers: Boolean = false) = synchronized(this) { - responseCallbacks.clear() - if (clearHandlers) { - messageHandlers.clear() - } - uniqueId.set(0) - isInjected = false - } - - fun release() { - removeJavascriptInterface() - consolePipe = null - responseCallbacks.clear() - messageHandlers.clear() - coroutineScope.launch { /* Cancel all ongoing coroutines */ }.cancel() + setupWebViewClient() } @SuppressLint("SetJavaScriptEnabled") private fun setupBridge() { webView.settings.javaScriptEnabled = true - webView.addJavascriptInterface(this, "normalPipe") - webView.addJavascriptInterface(this, "consolePipe") + webView.addJavascriptInterface(JsBridgeInterface(), "normalPipe") + webView.addJavascriptInterface(JsBridgeInterface(), "consolePipe") + Log.d(TAG, "Bridge setup completed") } - private fun removeJavascriptInterface() = synchronized(this) { - webView.removeJavascriptInterface("normalPipe") - webView.removeJavascriptInterface("consolePipe") - reset(true) - } - - @JavascriptInterface - fun postMessage(data: String?) { - data?.let { processMessage(it) } - } - - @JavascriptInterface - fun receiveConsole(data: String?) { - consolePipe?.post(data.orEmpty()) + private fun setupWebViewClient() { + webView.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + isWebViewReady.set(true) + Log.d(TAG, "WebView page finished loading") + injectJavascriptIfNeeded() + } + } } @MainThread - fun injectJavascript() { - if (!isInjected) { + private fun injectJavascriptIfNeeded() { + if (isInjected.get() || !isWebViewReady.get()) { + Log.d( + TAG, + "JavaScript injection skipped. Injected: ${isInjected.get()}, WebView ready: ${isWebViewReady.get()}", + ) + return + } + Log.d(TAG, "Injecting JavaScript") + webView.post { webView.evaluateJavascript("javascript:$bridgeScript", null) webView.evaluateJavascript("javascript:$consoleHookScript", null) - isInjected = true + isInjected.set(true) + Log.d(TAG, "JavaScript injection completed") } } fun registerHandler(handlerName: String, messageHandler: MessageHandler<*, *>) { - synchronized(messageHandlers) { - messageHandlers[handlerName] = messageHandler - } + messageHandlers[handlerName] = messageHandler + Log.d(TAG, "Handler registered: $handlerName") } fun removeHandler(handlerName: String) { - synchronized(messageHandlers) { - messageHandlers.remove(handlerName) - } + messageHandlers.remove(handlerName) + Log.d(TAG, "Handler removed: $handlerName") } @JvmOverloads fun callHandler(handlerName: String, data: Any? = null, callback: Callback<*>? = null) { + if (!isInjected.get()) { + Log.e(TAG, "Bridge is not injected. Cannot call handler: $handlerName") + return + } val callbackId = callback?.let { "native_cb_${uniqueId.incrementAndGet()}" } callbackId?.let { responseCallbacks[it] = callback } val message = CallMessage(handlerName, data, callbackId) val messageString = MessageSerializer.serializeCallMessage(message) dispatchMessage(messageString) + Log.d(TAG, "Handler called: $handlerName") } private fun processMessage(messageString: String) { - coroutineScope.launch(Dispatchers.Default) { - try { - val message = MessageSerializer.deserializeResponseMessage( - messageString, - responseCallbacks, - messageHandlers, - ) - when { - message.responseId != null -> handleResponse(message) - else -> handleRequest(message) - } - } catch (e: Exception) { - Log.e("[JsBridge]", "Error processing message: ${e.message}") + try { + val message = MessageSerializer.deserializeResponseMessage( + messageString, + responseCallbacks, + messageHandlers, + ) + when { + message.responseId != null -> handleResponse(message) + else -> handleRequest(message) } + } catch (e: Exception) { + Log.e(TAG, "Error processing message: ${e.message}") } } - private suspend fun handleResponse(responseMessage: ResponseMessage) { + private fun handleResponse(responseMessage: ResponseMessage) { val callback = responseCallbacks.remove(responseMessage.responseId) if (callback is Callback<*>) { @Suppress("UNCHECKED_CAST") (callback as Callback).onResult(responseMessage.responseData) + Log.d(TAG, "Response handled for ID: ${responseMessage.responseId}") } } - private suspend fun handleRequest(message: ResponseMessage) { + private fun handleRequest(message: ResponseMessage) { val handler = messageHandlers[message.handlerName] if (handler is MessageHandler<*, *>) { @Suppress("UNCHECKED_CAST") @@ -141,19 +133,94 @@ class WebViewJavascriptBridge @JvmOverloads constructor( val response = ResponseMessage(callbackId, responseData, null, null, null) val responseString = MessageSerializer.serializeResponseMessage(response) dispatchMessage(responseString) + Log.d(TAG, "Request handled: ${message.handlerName}") } + } else { + Log.e(TAG, "No handler found for: ${message.handlerName}") } } private fun dispatchMessage(messageString: String) { val script = "WebViewJavascriptBridge.handleMessageFromNative('$messageString');" - webView.post { webView.evaluateJavascript(script, null) } + webView.post { + webView.evaluateJavascript(script, null) + Log.d(TAG, "Message dispatched to JavaScript") + } } - private fun loadAsset(fileName: String): String = runCatching { + private fun loadAsset(fileName: String): String = try { context.assets.open(fileName).bufferedReader().use { it.readText() } - }.getOrElse { - Log.e("[JsBridge]", "Error loading asset $fileName: ${it.message}") + } catch (e: Exception) { + Log.e(TAG, "Error loading asset $fileName: ${e.message}") "" }.trimIndent() + + private fun clearState() { + responseCallbacks.clear() + uniqueId.set(0) + isInjected.set(false) + isWebViewReady.set(false) + Log.d(TAG, "Bridge state cleared") + } + + private fun removeJavascriptInterface() { + webView.removeJavascriptInterface("normalPipe") + webView.removeJavascriptInterface("consolePipe") + Log.d(TAG, "JavaScript interfaces removed") + } + + private fun release() { + removeJavascriptInterface() + consolePipe = null + responseCallbacks.clear() + messageHandlers.clear() + clearState() + Log.d(TAG, "Bridge released") + } + + fun reinitialize() { + release() + setupBridge() + setupWebViewClient() + Log.d(TAG, "Bridge reinitialized") + } + + override fun onResume(owner: LifecycleOwner) { + Log.d(TAG, "onResume") + injectJavascriptIfNeeded() + } + + override fun onDestroy(owner: LifecycleOwner) { + Log.d(TAG, "onDestroy") + release() + } + + private inner class JsBridgeInterface { + @JavascriptInterface + fun postMessage(data: String?) { + data?.let { + Log.d(TAG, "Message received from JavaScript: $it") + processMessage(it) + } + } + + @JavascriptInterface + fun receiveConsole(data: String?) { + Log.d(TAG, "Console message received: $data") + consolePipe?.post(data.orEmpty()) + } + } + + companion object { + private const val TAG = "WebViewJsBridge" + + fun create( + context: Context, + webView: WebView, + lifecycle: Lifecycle? = null, + ): WebViewJavascriptBridge = WebViewJavascriptBridge(context, webView).also { bridge -> + lifecycle?.addObserver(bridge) + Log.d(TAG, "Bridge created and lifecycle observer added") + } + } }