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

feat(bridge): Improve message handling and lifecycle management #5

Merged
merged 2 commits into from
Aug 25, 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
10 changes: 7 additions & 3 deletions app/src/main/java/com/ding1ding/jsbridge/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,15 @@ class MainActivity :
}

private fun setupWebViewBridge(webView: WebView) {
bridge = WebViewJavascriptBridge.create(this, webView, lifecycle).apply {
bridge = WebViewJavascriptBridge.create(
context = this,
webView = webView,
lifecycle = lifecycle,
webViewClient = createWebViewClient(),
).apply {
consolePipe = object : ConsolePipe {
override fun post(message: String) {
Logger.d("[console.log]") { message }
Logger.i("[console.log]") { message }
}
}

Expand All @@ -124,7 +129,6 @@ class MainActivity :

override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
bridge.injectJavascriptIfNeeded()
Logger.d(TAG) { "onPageFinished" }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ 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.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

class WebViewJavascriptBridge private constructor(
private val context: Context,
Expand All @@ -30,6 +36,8 @@ class WebViewJavascriptBridge private constructor(
private val isInjected = AtomicBoolean(false)
private val isWebViewReady = AtomicBoolean(false)

private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

init {
setupBridge()
}
Expand Down Expand Up @@ -60,8 +68,7 @@ class WebViewJavascriptBridge private constructor(
),
)
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean =
client?.shouldOverrideUrlLoading(view, url)
?: super.shouldOverrideUrlLoading(view, url)
client?.shouldOverrideUrlLoading(view, url) ?: super.shouldOverrideUrlLoading(view, url)

// Add other WebViewClient methods as needed, delegating to the client if it's not null
}
Expand All @@ -76,7 +83,7 @@ class WebViewJavascriptBridge private constructor(
return
}
Logger.d { "Injecting JavaScript" }
webView.post {
scope.launch {
webView.evaluateJavascript("javascript:$bridgeScript", null)
webView.evaluateJavascript("javascript:$consoleHookScript", null)
isInjected.set(true)
Expand All @@ -100,28 +107,30 @@ class WebViewJavascriptBridge private constructor(
Logger.e { "Bridge is not injected. Cannot call handler: $handlerName" }
return
}
val callbackId = callback?.let { "native_cb_${uniqueId.incrementAndGet()}" }
callbackId?.let { responseCallbacks[it] = callback }

val callbackId = callback?.let { "native_cb_${uniqueId.incrementAndGet()}" }?.also {
responseCallbacks[it] = callback
}
val message = CallMessage(handlerName, data, callbackId)
val messageString = MessageSerializer.serializeCallMessage(message)
dispatchMessage(messageString)
Logger.d { "Handler called: $handlerName" }
}

private fun processMessage(messageString: String) {
try {
val message = MessageSerializer.deserializeResponseMessage(
messageString,
responseCallbacks,
messageHandlers,
)
when {
message.responseId != null -> handleResponse(message)
else -> handleRequest(message)
scope.launch {
try {
val message = MessageSerializer.deserializeResponseMessage(
messageString,
responseCallbacks,
messageHandlers,
)
when {
message.responseId != null -> handleResponse(message)
else -> handleRequest(message)
}
} catch (e: Exception) {
Logger.e(e) { "Error processing message" }
}
} catch (e: Exception) {
Logger.e(e) { "Error processing message" }
}
}

Expand All @@ -135,36 +144,34 @@ class WebViewJavascriptBridge private constructor(
}

private fun handleRequest(message: ResponseMessage) {
val handler = messageHandlers[message.handlerName]
if (handler is MessageHandler<*, *>) {
messageHandlers[message.handlerName]?.let { handler ->
@Suppress("UNCHECKED_CAST")
val typedMessageHandler = handler as MessageHandler<Any?, Any?>
val responseData = typedMessageHandler.handle(message.data)
val responseData = (handler as MessageHandler<Any?, Any?>).handle(message.data)
message.callbackId?.let { callbackId ->
val response = ResponseMessage(callbackId, responseData, null, null, null)
val responseString = MessageSerializer.serializeResponseMessage(response)
dispatchMessage(responseString)
Logger.d { "Request handled: ${message.handlerName}" }
}
} else {
Logger.e { "No handler found for: ${message.handlerName}" }
}
} ?: Logger.e { "No handler found for: ${message.handlerName}" }
}

private fun dispatchMessage(messageString: String) {
val script = "WebViewJavascriptBridge.handleMessageFromNative('$messageString');"
webView.post {
scope.launch {
val script = "WebViewJavascriptBridge.handleMessageFromNative('$messageString');"
webView.evaluateJavascript(script, null)
Logger.d { "Message dispatched to JavaScript" }
}
}

private fun loadAsset(fileName: String): String = try {
context.assets.open(fileName).bufferedReader().use { it.readText() }
} catch (e: Exception) {
Logger.e(e) { "Error loading asset $fileName" }
""
}.trimIndent()
private fun loadAsset(fileName: String): String = runBlocking(Dispatchers.IO) {
try {
context.assets.open(fileName).bufferedReader().use { it.readText() }.trimIndent()
} catch (e: Exception) {
Logger.e(e) { "Error loading asset $fileName" }
""
}
}

private fun clearState() {
responseCallbacks.clear()
Expand All @@ -174,18 +181,14 @@ class WebViewJavascriptBridge private constructor(
Logger.d { "Bridge state cleared" }
}

private fun removeJavascriptInterface() {
private fun release() {
webView.removeJavascriptInterface("normalPipe")
webView.removeJavascriptInterface("consolePipe")
Logger.d { "JavaScript interfaces removed" }
}

private fun release() {
removeJavascriptInterface()
consolePipe = null
responseCallbacks.clear()
messageHandlers.clear()
clearState()
scope.cancel()
Logger.d { "Bridge released" }
}

Expand Down Expand Up @@ -222,17 +225,19 @@ class WebViewJavascriptBridge private constructor(
}

companion object {
@JvmStatic
fun create(
context: Context,
webView: WebView,
lifecycle: Lifecycle? = null,
webViewClient: WebViewClient? = null,
): WebViewJavascriptBridge = WebViewJavascriptBridge(context, webView).also { bridge ->
lifecycle?.addObserver(bridge)
bridge.setWebViewClient(webViewClient)
): WebViewJavascriptBridge = WebViewJavascriptBridge(context, webView).apply {
lifecycle?.addObserver(this)
setWebViewClient(webViewClient)
Logger.d { "Bridge created and lifecycle observer added" }
}

@JvmStatic
fun setLogLevel(level: Logger.LogLevel) {
Logger.logLevel = level
}
Expand Down
Loading