@@ -5,133 +5,125 @@ import android.content.Context
5
5
import android.util.Log
6
6
import android.webkit.JavascriptInterface
7
7
import android.webkit.WebView
8
+ import android.webkit.WebViewClient
8
9
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
9
15
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
14
16
15
- class WebViewJavascriptBridge @JvmOverloads constructor(
17
+ class WebViewJavascriptBridge private constructor(
16
18
private val context : Context ,
17
19
private val webView : WebView ,
18
- private val coroutineScope : CoroutineScope = CoroutineScope (SupervisorJob () + Dispatchers .Main ),
19
- ) {
20
- @JvmField
20
+ ) : DefaultLifecycleObserver {
21
+
21
22
var consolePipe: ConsolePipe ? = null
22
23
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 <* , * >>()
25
26
private val uniqueId = AtomicInteger (0 )
26
27
27
28
private val bridgeScript by lazy { loadAsset(" bridge.js" ) }
28
29
private val consoleHookScript by lazy { loadAsset(" hookConsole.js" ) }
29
30
30
- private var isInjected = false
31
+ private val isInjected = AtomicBoolean (false )
32
+ private val isWebViewReady = AtomicBoolean (false )
31
33
32
34
init {
33
35
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()
52
37
}
53
38
54
39
@SuppressLint(" SetJavaScriptEnabled" )
55
40
private fun setupBridge () {
56
41
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" )
59
45
}
60
46
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
+ }
75
56
}
76
57
77
58
@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 {
80
69
webView.evaluateJavascript(" javascript:$bridgeScript " , null )
81
70
webView.evaluateJavascript(" javascript:$consoleHookScript " , null )
82
- isInjected = true
71
+ isInjected.set(true )
72
+ Log .d(TAG , " JavaScript injection completed" )
83
73
}
84
74
}
85
75
86
76
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 " )
90
79
}
91
80
92
81
fun removeHandler (handlerName : String ) {
93
- synchronized(messageHandlers) {
94
- messageHandlers.remove(handlerName)
95
- }
82
+ messageHandlers.remove(handlerName)
83
+ Log .d(TAG , " Handler removed: $handlerName " )
96
84
}
97
85
98
86
@JvmOverloads
99
87
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
+ }
100
92
val callbackId = callback?.let { " native_cb_${uniqueId.incrementAndGet()} " }
101
93
callbackId?.let { responseCallbacks[it] = callback }
102
94
103
95
val message = CallMessage (handlerName, data, callbackId)
104
96
val messageString = MessageSerializer .serializeCallMessage(message)
105
97
dispatchMessage(messageString)
98
+ Log .d(TAG , " Handler called: $handlerName " )
106
99
}
107
100
108
101
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)
122
111
}
112
+ } catch (e: Exception ) {
113
+ Log .e(TAG , " Error processing message: ${e.message} " )
123
114
}
124
115
}
125
116
126
- private suspend fun handleResponse (responseMessage : ResponseMessage ) {
117
+ private fun handleResponse (responseMessage : ResponseMessage ) {
127
118
val callback = responseCallbacks.remove(responseMessage.responseId)
128
119
if (callback is Callback <* >) {
129
120
@Suppress(" UNCHECKED_CAST" )
130
121
(callback as Callback <Any ?>).onResult(responseMessage.responseData)
122
+ Log .d(TAG , " Response handled for ID: ${responseMessage.responseId} " )
131
123
}
132
124
}
133
125
134
- private suspend fun handleRequest (message : ResponseMessage ) {
126
+ private fun handleRequest (message : ResponseMessage ) {
135
127
val handler = messageHandlers[message.handlerName]
136
128
if (handler is MessageHandler <* , * >) {
137
129
@Suppress(" UNCHECKED_CAST" )
@@ -141,19 +133,94 @@ class WebViewJavascriptBridge @JvmOverloads constructor(
141
133
val response = ResponseMessage (callbackId, responseData, null , null , null )
142
134
val responseString = MessageSerializer .serializeResponseMessage(response)
143
135
dispatchMessage(responseString)
136
+ Log .d(TAG , " Request handled: ${message.handlerName} " )
144
137
}
138
+ } else {
139
+ Log .e(TAG , " No handler found for: ${message.handlerName} " )
145
140
}
146
141
}
147
142
148
143
private fun dispatchMessage (messageString : String ) {
149
144
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
+ }
151
149
}
152
150
153
- private fun loadAsset (fileName : String ): String = runCatching {
151
+ private fun loadAsset (fileName : String ): String = try {
154
152
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} " )
157
155
" "
158
156
}.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
+ }
159
226
}
0 commit comments