Skip to content

Commit

Permalink
Merge pull request #2 from ding1dingx/dev
Browse files Browse the repository at this point in the history
feat(WebViewJavascriptBridge): Improve setup and lifecycle management
  • Loading branch information
syxc authored Aug 25, 2024
2 parents 1c8ee8f + 8e54213 commit f0b6c0f
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 81 deletions.
14 changes: 6 additions & 8 deletions app/src/main/java/com/ding1ding/jsbridge/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class MainActivity :
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupWebView()
setupBridge()
setupClickListeners()
}

Expand Down Expand Up @@ -56,9 +55,6 @@ class MainActivity :
}

override fun onDestroy() {
// 01
bridge.release()
// 02
releaseWebView()
Log.d(TAG, "onDestroy")
super.onDestroy()
Expand All @@ -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)
Expand All @@ -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?) {
Expand Down
213 changes: 140 additions & 73 deletions library/src/main/java/com/ding1ding/jsbridge/WebViewJavascriptBridge.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Callback<*>>()
private val messageHandlers = mutableMapOf<String, MessageHandler<*, *>>()
private val responseCallbacks = ConcurrentHashMap<String, Callback<*>>()
private val messageHandlers = ConcurrentHashMap<String, MessageHandler<*, *>>()
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<Any?>).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")
Expand All @@ -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")
}
}
}

0 comments on commit f0b6c0f

Please sign in to comment.