Skip to content

Commit f0b6c0f

Browse files
authored
Merge pull request #2 from ding1dingx/dev
feat(WebViewJavascriptBridge): Improve setup and lifecycle management
2 parents 1c8ee8f + 8e54213 commit f0b6c0f

File tree

2 files changed

+146
-81
lines changed

2 files changed

+146
-81
lines changed

app/src/main/java/com/ding1ding/jsbridge/app/MainActivity.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ class MainActivity :
2828
super.onCreate(savedInstanceState)
2929
setContentView(R.layout.activity_main)
3030
setupWebView()
31-
setupBridge()
3231
setupClickListeners()
3332
}
3433

@@ -56,9 +55,6 @@ class MainActivity :
5655
}
5756

5857
override fun onDestroy() {
59-
// 01
60-
bridge.release()
61-
// 02
6258
releaseWebView()
6359
Log.d(TAG, "onDestroy")
6460
super.onDestroy()
@@ -84,14 +80,17 @@ class MainActivity :
8480
allowUniversalAccessFromFileURLs = true
8581
}
8682
webViewClient = createWebViewClient()
87-
loadUrl("file:///android_asset/index.html")
8883
}
8984

9085
webViewContainer.addView(webView)
86+
87+
setupWebViewBridge(webView)
88+
89+
webView.loadUrl("file:///android_asset/index.html")
9190
}
9291

93-
private fun setupBridge() {
94-
bridge = WebViewJavascriptBridge(this, webView).apply {
92+
private fun setupWebViewBridge(webView: WebView) {
93+
bridge = WebViewJavascriptBridge.create(this, webView, lifecycle).apply {
9594
consolePipe = object : ConsolePipe {
9695
override fun post(message: String) {
9796
Log.d("[console.log]", message)
@@ -112,7 +111,6 @@ class MainActivity :
112111
private fun createWebViewClient() = object : WebViewClient() {
113112
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
114113
Log.d(TAG, "onPageStarted")
115-
bridge.injectJavascript()
116114
}
117115

118116
override fun onPageFinished(view: WebView?, url: String?) {

library/src/main/java/com/ding1ding/jsbridge/WebViewJavascriptBridge.kt

Lines changed: 140 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,133 +5,125 @@ import android.content.Context
55
import android.util.Log
66
import android.webkit.JavascriptInterface
77
import android.webkit.WebView
8+
import android.webkit.WebViewClient
89
import androidx.annotation.MainThread
10+
import androidx.lifecycle.DefaultLifecycleObserver
11+
import androidx.lifecycle.Lifecycle
12+
import androidx.lifecycle.LifecycleOwner
13+
import java.util.concurrent.ConcurrentHashMap
14+
import java.util.concurrent.atomic.AtomicBoolean
915
import java.util.concurrent.atomic.AtomicInteger
10-
import kotlinx.coroutines.CoroutineScope
11-
import kotlinx.coroutines.Dispatchers
12-
import kotlinx.coroutines.SupervisorJob
13-
import kotlinx.coroutines.launch
1416

15-
class WebViewJavascriptBridge @JvmOverloads constructor(
17+
class WebViewJavascriptBridge private constructor(
1618
private val context: Context,
1719
private val webView: WebView,
18-
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main),
19-
) {
20-
@JvmField
20+
) : DefaultLifecycleObserver {
21+
2122
var consolePipe: ConsolePipe? = null
2223

23-
private val responseCallbacks = mutableMapOf<String, Callback<*>>()
24-
private val messageHandlers = mutableMapOf<String, MessageHandler<*, *>>()
24+
private val responseCallbacks = ConcurrentHashMap<String, Callback<*>>()
25+
private val messageHandlers = ConcurrentHashMap<String, MessageHandler<*, *>>()
2526
private val uniqueId = AtomicInteger(0)
2627

2728
private val bridgeScript by lazy { loadAsset("bridge.js") }
2829
private val consoleHookScript by lazy { loadAsset("hookConsole.js") }
2930

30-
private var isInjected = false
31+
private val isInjected = AtomicBoolean(false)
32+
private val isWebViewReady = AtomicBoolean(false)
3133

3234
init {
3335
setupBridge()
34-
}
35-
36-
@JvmOverloads
37-
fun reset(clearHandlers: Boolean = false) = synchronized(this) {
38-
responseCallbacks.clear()
39-
if (clearHandlers) {
40-
messageHandlers.clear()
41-
}
42-
uniqueId.set(0)
43-
isInjected = false
44-
}
45-
46-
fun release() {
47-
removeJavascriptInterface()
48-
consolePipe = null
49-
responseCallbacks.clear()
50-
messageHandlers.clear()
51-
coroutineScope.launch { /* Cancel all ongoing coroutines */ }.cancel()
36+
setupWebViewClient()
5237
}
5338

5439
@SuppressLint("SetJavaScriptEnabled")
5540
private fun setupBridge() {
5641
webView.settings.javaScriptEnabled = true
57-
webView.addJavascriptInterface(this, "normalPipe")
58-
webView.addJavascriptInterface(this, "consolePipe")
42+
webView.addJavascriptInterface(JsBridgeInterface(), "normalPipe")
43+
webView.addJavascriptInterface(JsBridgeInterface(), "consolePipe")
44+
Log.d(TAG, "Bridge setup completed")
5945
}
6046

61-
private fun removeJavascriptInterface() = synchronized(this) {
62-
webView.removeJavascriptInterface("normalPipe")
63-
webView.removeJavascriptInterface("consolePipe")
64-
reset(true)
65-
}
66-
67-
@JavascriptInterface
68-
fun postMessage(data: String?) {
69-
data?.let { processMessage(it) }
70-
}
71-
72-
@JavascriptInterface
73-
fun receiveConsole(data: String?) {
74-
consolePipe?.post(data.orEmpty())
47+
private fun setupWebViewClient() {
48+
webView.webViewClient = object : WebViewClient() {
49+
override fun onPageFinished(view: WebView?, url: String?) {
50+
super.onPageFinished(view, url)
51+
isWebViewReady.set(true)
52+
Log.d(TAG, "WebView page finished loading")
53+
injectJavascriptIfNeeded()
54+
}
55+
}
7556
}
7657

7758
@MainThread
78-
fun injectJavascript() {
79-
if (!isInjected) {
59+
private fun injectJavascriptIfNeeded() {
60+
if (isInjected.get() || !isWebViewReady.get()) {
61+
Log.d(
62+
TAG,
63+
"JavaScript injection skipped. Injected: ${isInjected.get()}, WebView ready: ${isWebViewReady.get()}",
64+
)
65+
return
66+
}
67+
Log.d(TAG, "Injecting JavaScript")
68+
webView.post {
8069
webView.evaluateJavascript("javascript:$bridgeScript", null)
8170
webView.evaluateJavascript("javascript:$consoleHookScript", null)
82-
isInjected = true
71+
isInjected.set(true)
72+
Log.d(TAG, "JavaScript injection completed")
8373
}
8474
}
8575

8676
fun registerHandler(handlerName: String, messageHandler: MessageHandler<*, *>) {
87-
synchronized(messageHandlers) {
88-
messageHandlers[handlerName] = messageHandler
89-
}
77+
messageHandlers[handlerName] = messageHandler
78+
Log.d(TAG, "Handler registered: $handlerName")
9079
}
9180

9281
fun removeHandler(handlerName: String) {
93-
synchronized(messageHandlers) {
94-
messageHandlers.remove(handlerName)
95-
}
82+
messageHandlers.remove(handlerName)
83+
Log.d(TAG, "Handler removed: $handlerName")
9684
}
9785

9886
@JvmOverloads
9987
fun callHandler(handlerName: String, data: Any? = null, callback: Callback<*>? = null) {
88+
if (!isInjected.get()) {
89+
Log.e(TAG, "Bridge is not injected. Cannot call handler: $handlerName")
90+
return
91+
}
10092
val callbackId = callback?.let { "native_cb_${uniqueId.incrementAndGet()}" }
10193
callbackId?.let { responseCallbacks[it] = callback }
10294

10395
val message = CallMessage(handlerName, data, callbackId)
10496
val messageString = MessageSerializer.serializeCallMessage(message)
10597
dispatchMessage(messageString)
98+
Log.d(TAG, "Handler called: $handlerName")
10699
}
107100

108101
private fun processMessage(messageString: String) {
109-
coroutineScope.launch(Dispatchers.Default) {
110-
try {
111-
val message = MessageSerializer.deserializeResponseMessage(
112-
messageString,
113-
responseCallbacks,
114-
messageHandlers,
115-
)
116-
when {
117-
message.responseId != null -> handleResponse(message)
118-
else -> handleRequest(message)
119-
}
120-
} catch (e: Exception) {
121-
Log.e("[JsBridge]", "Error processing message: ${e.message}")
102+
try {
103+
val message = MessageSerializer.deserializeResponseMessage(
104+
messageString,
105+
responseCallbacks,
106+
messageHandlers,
107+
)
108+
when {
109+
message.responseId != null -> handleResponse(message)
110+
else -> handleRequest(message)
122111
}
112+
} catch (e: Exception) {
113+
Log.e(TAG, "Error processing message: ${e.message}")
123114
}
124115
}
125116

126-
private suspend fun handleResponse(responseMessage: ResponseMessage) {
117+
private fun handleResponse(responseMessage: ResponseMessage) {
127118
val callback = responseCallbacks.remove(responseMessage.responseId)
128119
if (callback is Callback<*>) {
129120
@Suppress("UNCHECKED_CAST")
130121
(callback as Callback<Any?>).onResult(responseMessage.responseData)
122+
Log.d(TAG, "Response handled for ID: ${responseMessage.responseId}")
131123
}
132124
}
133125

134-
private suspend fun handleRequest(message: ResponseMessage) {
126+
private fun handleRequest(message: ResponseMessage) {
135127
val handler = messageHandlers[message.handlerName]
136128
if (handler is MessageHandler<*, *>) {
137129
@Suppress("UNCHECKED_CAST")
@@ -141,19 +133,94 @@ class WebViewJavascriptBridge @JvmOverloads constructor(
141133
val response = ResponseMessage(callbackId, responseData, null, null, null)
142134
val responseString = MessageSerializer.serializeResponseMessage(response)
143135
dispatchMessage(responseString)
136+
Log.d(TAG, "Request handled: ${message.handlerName}")
144137
}
138+
} else {
139+
Log.e(TAG, "No handler found for: ${message.handlerName}")
145140
}
146141
}
147142

148143
private fun dispatchMessage(messageString: String) {
149144
val script = "WebViewJavascriptBridge.handleMessageFromNative('$messageString');"
150-
webView.post { webView.evaluateJavascript(script, null) }
145+
webView.post {
146+
webView.evaluateJavascript(script, null)
147+
Log.d(TAG, "Message dispatched to JavaScript")
148+
}
151149
}
152150

153-
private fun loadAsset(fileName: String): String = runCatching {
151+
private fun loadAsset(fileName: String): String = try {
154152
context.assets.open(fileName).bufferedReader().use { it.readText() }
155-
}.getOrElse {
156-
Log.e("[JsBridge]", "Error loading asset $fileName: ${it.message}")
153+
} catch (e: Exception) {
154+
Log.e(TAG, "Error loading asset $fileName: ${e.message}")
157155
""
158156
}.trimIndent()
157+
158+
private fun clearState() {
159+
responseCallbacks.clear()
160+
uniqueId.set(0)
161+
isInjected.set(false)
162+
isWebViewReady.set(false)
163+
Log.d(TAG, "Bridge state cleared")
164+
}
165+
166+
private fun removeJavascriptInterface() {
167+
webView.removeJavascriptInterface("normalPipe")
168+
webView.removeJavascriptInterface("consolePipe")
169+
Log.d(TAG, "JavaScript interfaces removed")
170+
}
171+
172+
private fun release() {
173+
removeJavascriptInterface()
174+
consolePipe = null
175+
responseCallbacks.clear()
176+
messageHandlers.clear()
177+
clearState()
178+
Log.d(TAG, "Bridge released")
179+
}
180+
181+
fun reinitialize() {
182+
release()
183+
setupBridge()
184+
setupWebViewClient()
185+
Log.d(TAG, "Bridge reinitialized")
186+
}
187+
188+
override fun onResume(owner: LifecycleOwner) {
189+
Log.d(TAG, "onResume")
190+
injectJavascriptIfNeeded()
191+
}
192+
193+
override fun onDestroy(owner: LifecycleOwner) {
194+
Log.d(TAG, "onDestroy")
195+
release()
196+
}
197+
198+
private inner class JsBridgeInterface {
199+
@JavascriptInterface
200+
fun postMessage(data: String?) {
201+
data?.let {
202+
Log.d(TAG, "Message received from JavaScript: $it")
203+
processMessage(it)
204+
}
205+
}
206+
207+
@JavascriptInterface
208+
fun receiveConsole(data: String?) {
209+
Log.d(TAG, "Console message received: $data")
210+
consolePipe?.post(data.orEmpty())
211+
}
212+
}
213+
214+
companion object {
215+
private const val TAG = "WebViewJsBridge"
216+
217+
fun create(
218+
context: Context,
219+
webView: WebView,
220+
lifecycle: Lifecycle? = null,
221+
): WebViewJavascriptBridge = WebViewJavascriptBridge(context, webView).also { bridge ->
222+
lifecycle?.addObserver(bridge)
223+
Log.d(TAG, "Bridge created and lifecycle observer added")
224+
}
225+
}
159226
}

0 commit comments

Comments
 (0)