Skip to content

Commit 0fe3935

Browse files
committed
Add delegate to simplify creating implementations of WebMessagingPlugin
1 parent e5f4f80 commit 0fe3935

File tree

7 files changed

+344
-237
lines changed

7 files changed

+344
-237
lines changed

content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/messaging/ContentScopeScriptsWebMessagingPlugin.kt

Lines changed: 23 additions & 212 deletions
Original file line numberDiff line numberDiff line change
@@ -16,236 +16,47 @@
1616

1717
package com.duckduckgo.contentscopescripts.impl.messaging
1818

19-
import android.annotation.SuppressLint
20-
import android.webkit.WebView
21-
import androidx.annotation.VisibleForTesting
22-
import androidx.lifecycle.findViewTreeLifecycleOwner
23-
import androidx.lifecycle.lifecycleScope
24-
import androidx.webkit.JavaScriptReplyProxy
25-
import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper
2619
import com.duckduckgo.common.utils.plugins.PluginPoint
2720
import com.duckduckgo.contentscopescripts.api.WebViewCompatContentScopeJsMessageHandlersPlugin
2821
import com.duckduckgo.contentscopescripts.impl.WebViewCompatContentScopeScripts
2922
import com.duckduckgo.di.scopes.FragmentScope
30-
import com.duckduckgo.js.messaging.api.JsCallbackData
31-
import com.duckduckgo.js.messaging.api.JsMessage
32-
import com.duckduckgo.js.messaging.api.ProcessResult.SendResponse
33-
import com.duckduckgo.js.messaging.api.ProcessResult.SendToConsumer
34-
import com.duckduckgo.js.messaging.api.SubscriptionEvent
35-
import com.duckduckgo.js.messaging.api.SubscriptionEventData
23+
import com.duckduckgo.js.messaging.api.GlobalJsMessageHandler
3624
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
37-
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
25+
import com.duckduckgo.js.messaging.api.WebMessagingPluginDelegate
26+
import com.duckduckgo.js.messaging.api.WebMessagingPluginStrategy
27+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler
3828
import com.squareup.anvil.annotations.ContributesBinding
3929
import com.squareup.anvil.annotations.ContributesMultibinding
40-
import com.squareup.moshi.Moshi
4130
import dagger.SingleInstanceIn
42-
import kotlinx.coroutines.launch
43-
import logcat.LogPriority.ERROR
44-
import logcat.asLog
45-
import logcat.logcat
46-
import org.json.JSONObject
4731
import javax.inject.Inject
4832
import javax.inject.Named
4933

50-
private const val JS_OBJECT_NAME = "contentScopeAdsjs"
51-
5234
@Named("contentScopeScripts")
5335
@SingleInstanceIn(FragmentScope::class)
5436
@ContributesBinding(FragmentScope::class)
5537
@ContributesMultibinding(scope = FragmentScope::class, ignoreQualifier = true)
5638
class ContentScopeScriptsWebMessagingPlugin @Inject constructor(
57-
private val handlers: PluginPoint<WebViewCompatContentScopeJsMessageHandlersPlugin>,
58-
private val globalHandlers: PluginPoint<GlobalContentScopeJsMessageHandlersPlugin>,
59-
private val webViewCompatContentScopeScripts: WebViewCompatContentScopeScripts,
60-
private val webViewCompatWrapper: WebViewCompatWrapper,
61-
) : WebMessagingPlugin {
62-
private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build()
63-
64-
override val context: String = "contentScopeScripts"
65-
private val allowedDomains: Set<String> = setOf("*")
66-
67-
private var globalReplyProxy: JavaScriptReplyProxy? = null
68-
69-
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
70-
internal fun process(
71-
webView: WebView,
72-
message: String,
73-
jsMessageCallback: WebViewCompatMessageCallback,
74-
replyProxy: JavaScriptReplyProxy,
75-
) {
76-
try {
77-
val adapter = moshi.adapter(JsMessage::class.java)
78-
val jsMessage = adapter.fromJson(message)
79-
80-
jsMessage?.let {
81-
if (context == jsMessage.context) {
82-
// Setup reply proxy so we can send subscription events
83-
if (jsMessage.featureName == "messaging" && jsMessage.method == "initialPing") {
84-
globalReplyProxy = replyProxy
85-
}
86-
87-
// Process global handlers first (always processed regardless of feature handlers)
88-
globalHandlers
89-
.getPlugins()
90-
.map { it.getGlobalJsMessageHandler() }
91-
.filter { it.method == jsMessage.method }
92-
.forEach { handler ->
93-
handler.process(jsMessage)?.let { processResult ->
94-
when (processResult) {
95-
is SendToConsumer -> {
96-
sendToConsumer(webView, jsMessageCallback, jsMessage, replyProxy)
97-
}
98-
is SendResponse -> {
99-
webView.findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
100-
onResponse(webView, jsMessage, replyProxy)
101-
}
102-
}
103-
}
104-
}
105-
}
106-
107-
// Process with feature handlers
108-
handlers
109-
.getPlugins()
110-
.map { it.getJsMessageHandler() }
111-
.firstOrNull {
112-
it.methods.contains(jsMessage.method) && it.featureName == jsMessage.featureName
113-
}?.process(jsMessage)
114-
?.let { processResult ->
115-
when (processResult) {
116-
is SendToConsumer -> {
117-
sendToConsumer(webView, jsMessageCallback, jsMessage, replyProxy)
118-
}
119-
is SendResponse -> {
120-
webView.findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
121-
onResponse(webView, jsMessage, replyProxy)
122-
}
123-
}
124-
}
125-
}
126-
}
127-
}
128-
} catch (e: Exception) {
129-
logcat(ERROR) { "Exception is ${e.asLog()}" }
39+
handlers: PluginPoint<WebViewCompatContentScopeJsMessageHandlersPlugin>,
40+
globalHandlers: PluginPoint<GlobalContentScopeJsMessageHandlersPlugin>,
41+
webViewCompatContentScopeScripts: WebViewCompatContentScopeScripts,
42+
webMessagingPluginDelegate: WebMessagingPluginDelegate,
43+
) : WebMessagingPlugin by webMessagingPluginDelegate.createPlugin(
44+
object : WebMessagingPluginStrategy {
45+
override val context: String = "contentScopeScripts"
46+
override val allowedDomains: Set<String> = setOf("*")
47+
override val objectName: String
48+
get() = "contentScopeAdsjs"
49+
50+
override suspend fun isEnabled(): Boolean {
51+
return webViewCompatContentScopeScripts.isEnabled()
13052
}
131-
}
132-
133-
private suspend fun onResponse(
134-
webView: WebView,
135-
jsMessage: JsMessage,
136-
replyProxy: JavaScriptReplyProxy,
137-
) {
138-
val callbackData =
139-
JsCallbackData(
140-
id = jsMessage.id ?: "",
141-
params = jsMessage.params,
142-
featureName = jsMessage.featureName,
143-
method = jsMessage.method,
144-
)
145-
onResponse(webView, callbackData, replyProxy)
146-
}
147-
148-
private fun sendToConsumer(
149-
webView: WebView,
150-
jsMessageCallback: WebViewCompatMessageCallback,
151-
jsMessage: JsMessage,
152-
replyProxy: JavaScriptReplyProxy,
153-
) {
154-
jsMessageCallback.process(
155-
context = context,
156-
featureName = jsMessage.featureName,
157-
method = jsMessage.method,
158-
id = jsMessage.id ?: "",
159-
data = jsMessage.params,
160-
onResponse = { response: JSONObject ->
161-
val callbackData =
162-
JsCallbackData(
163-
id = jsMessage.id ?: "",
164-
params = response,
165-
featureName = jsMessage.featureName,
166-
method = jsMessage.method,
167-
)
168-
onResponse(webView, callbackData, replyProxy)
169-
},
170-
)
171-
}
17253

173-
override suspend fun register(
174-
jsMessageCallback: WebViewCompatMessageCallback,
175-
webView: WebView,
176-
) {
177-
if (!webViewCompatContentScopeScripts.isEnabled()) {
178-
return
54+
override fun getMessageHandlers(): List<WebViewCompatMessageHandler> {
55+
return handlers.getPlugins().map { it.getJsMessageHandler() }
17956
}
18057

181-
runCatching {
182-
return@runCatching webViewCompatWrapper.addWebMessageListener(
183-
webView,
184-
JS_OBJECT_NAME,
185-
allowedDomains,
186-
) { _, message, _, _, replyProxy ->
187-
process(
188-
webView,
189-
message.data ?: "",
190-
jsMessageCallback,
191-
replyProxy,
192-
)
193-
}
194-
}.getOrElse { exception ->
195-
logcat(ERROR) { "Error adding WebMessageListener for contentScopeAdsjs: ${exception.asLog()}" }
196-
}
197-
}
198-
199-
override suspend fun unregister(webView: WebView) {
200-
if (!webViewCompatContentScopeScripts.isEnabled()) return
201-
runCatching {
202-
return@runCatching webViewCompatWrapper.removeWebMessageListener(webView, JS_OBJECT_NAME)
203-
}.getOrElse { exception ->
204-
logcat(ERROR) {
205-
"Error removing WebMessageListener for contentScopeAdsjs: ${exception.asLog()}"
206-
}
207-
}
208-
}
209-
210-
@SuppressLint("RequiresFeature")
211-
private suspend fun onResponse(
212-
webView: WebView,
213-
response: JsCallbackData,
214-
replyProxy: JavaScriptReplyProxy,
215-
) {
216-
runCatching {
217-
val responseWithId =
218-
JSONObject().apply {
219-
put("id", response.id)
220-
put("result", response.params)
221-
put("featureName", response.featureName)
222-
put("context", context)
223-
}
224-
webViewCompatWrapper.postMessage(webView, replyProxy, responseWithId.toString())
225-
}
226-
}
227-
228-
@SuppressLint("RequiresFeature")
229-
override suspend fun postMessage(
230-
webView: WebView,
231-
subscriptionEventData: SubscriptionEventData,
232-
) {
233-
runCatching {
234-
if (!webViewCompatContentScopeScripts.isEnabled()) {
235-
return
236-
}
237-
238-
val subscriptionEvent =
239-
SubscriptionEvent(
240-
context = context,
241-
featureName = subscriptionEventData.featureName,
242-
subscriptionName = subscriptionEventData.subscriptionName,
243-
params = subscriptionEventData.params,
244-
).let {
245-
moshi.adapter(SubscriptionEvent::class.java).toJson(it)
246-
}
247-
248-
webViewCompatWrapper.postMessage(webView, globalReplyProxy, subscriptionEvent)
58+
override fun getGlobalMessageHandler(): List<GlobalJsMessageHandler> {
59+
return globalHandlers.getPlugins().map { it.getGlobalJsMessageHandler() }
24960
}
250-
}
251-
}
61+
},
62+
)

content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/messaging/DebugFlagGlobalHandler.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.duckduckgo.contentscopescripts.impl.messaging
1818

1919
import com.duckduckgo.di.scopes.AppScope
20+
import com.duckduckgo.js.messaging.api.GlobalJsMessageHandler
2021
import com.duckduckgo.js.messaging.api.JsMessage
2122
import com.duckduckgo.js.messaging.api.ProcessResult
2223
import com.duckduckgo.js.messaging.api.ProcessResult.SendToConsumer

content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/messaging/GlobalContentScopeJsMessageHandlersPlugin.kt

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616

1717
package com.duckduckgo.contentscopescripts.impl.messaging
1818

19-
import com.duckduckgo.js.messaging.api.JsMessage
20-
import com.duckduckgo.js.messaging.api.ProcessResult
19+
import com.duckduckgo.js.messaging.api.GlobalJsMessageHandler
2120

2221
/**
2322
* Plugin interface for global message handlers that should always be processed
@@ -31,26 +30,3 @@ interface GlobalContentScopeJsMessageHandlersPlugin {
3130
*/
3231
fun getGlobalJsMessageHandler(): GlobalJsMessageHandler
3332
}
34-
35-
/**
36-
* Handler for global messages that should be processed for all features.
37-
*/
38-
interface GlobalJsMessageHandler {
39-
40-
/**
41-
* Processes a global message received by the WebView.
42-
*
43-
* This method is responsible for handling a [JsMessage] and optionally
44-
* invoking a callback so consumers can also process the message if needed.
45-
*
46-
* @param jsMessage The JavaScript message to be processed.
47-
*/
48-
fun process(
49-
jsMessage: JsMessage,
50-
): ProcessResult?
51-
52-
/**
53-
* Method this handler can process.
54-
*/
55-
val method: String
56-
}

js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/WebMessagingPlugin.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,42 @@ interface WebMessagingPlugin {
3333

3434
val context: String
3535
}
36+
37+
interface WebMessagingPluginDelegate {
38+
39+
/**
40+
* Creates a [WebMessagingPlugin] implementation with the given [WebMessagingPluginStrategy].
41+
* @param strategy the strategy to use for web messaging behavior
42+
* @return [WebMessagingPlugin] implementation
43+
*/
44+
fun createPlugin(strategy: WebMessagingPluginStrategy): WebMessagingPlugin
45+
}
46+
47+
/**
48+
* Strategy interface for web messaging logic.
49+
* Allows different implementations to provide their own behavior.
50+
*/
51+
interface WebMessagingPluginStrategy {
52+
val context: String
53+
val allowedDomains: Set<String>
54+
val objectName: String
55+
56+
/**
57+
* Determines whether messaging actions should proceed (i.e. by checking feature flags).
58+
* @return true if messaging is allowed, false otherwise
59+
*/
60+
suspend fun isEnabled(): Boolean
61+
62+
/**
63+
* Provides the list of message handlers to process incoming messages.
64+
* @return list of [WebViewCompatMessageHandler] implementations
65+
*/
66+
fun getMessageHandlers(): List<WebViewCompatMessageHandler>
67+
68+
/**
69+
* Provides the list of global message handlers that should always be processed
70+
* regardless of whether a specific feature handler matches the message.
71+
* @return list of [GlobalJsMessageHandler] implementations
72+
*/
73+
fun getGlobalMessageHandler(): List<GlobalJsMessageHandler>
74+
}

js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/WebViewCompatMessaging.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,26 @@ interface WebViewCompatMessageHandler {
7373
*/
7474
val methods: List<String>
7575
}
76+
77+
/**
78+
* Handler for global messages that should be processed for all features.
79+
*/
80+
interface GlobalJsMessageHandler {
81+
82+
/**
83+
* Processes a global message received by the WebView.
84+
*
85+
* This method is responsible for handling a [JsMessage] and optionally
86+
* invoking a callback so consumers can also process the message if needed.
87+
*
88+
* @param jsMessage The JavaScript message to be processed.
89+
*/
90+
fun process(
91+
jsMessage: JsMessage,
92+
): ProcessResult?
93+
94+
/**
95+
* Method this handler can process.
96+
*/
97+
val method: String
98+
}

js-messaging/js-messaging-impl/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies {
3131
anvil project(':anvil-compiler')
3232
implementation project(':anvil-annotations')
3333

34+
implementation AndroidX.webkit
3435
implementation "com.squareup.logcat:logcat:_"
3536
implementation KotlinX.coroutines.core
3637
implementation Google.dagger

0 commit comments

Comments
 (0)