Skip to content

Commit d9a8511

Browse files
committed
Add delegate to simplify creating implementations of WebMessagingPlugin
1 parent 27eb1c3 commit d9a8511

File tree

7 files changed

+322
-230
lines changed

7 files changed

+322
-230
lines changed

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

Lines changed: 23 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -16,229 +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.webkit.JavaScriptReplyProxy
23-
import com.duckduckgo.app.di.AppCoroutineScope
24-
import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper
25-
import com.duckduckgo.common.utils.DispatcherProvider
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
4231
import javax.inject.Inject
4332
import javax.inject.Named
44-
import kotlinx.coroutines.CoroutineScope
45-
import kotlinx.coroutines.launch
46-
import kotlinx.coroutines.withContext
47-
import logcat.LogPriority.ERROR
48-
import logcat.asLog
49-
import logcat.logcat
50-
import org.json.JSONObject
51-
52-
private const val JS_OBJECT_NAME = "contentScopeAdsjs"
5333

5434
@Named("contentScopeScripts")
5535
@SingleInstanceIn(FragmentScope::class)
5636
@ContributesBinding(FragmentScope::class)
5737
@ContributesMultibinding(scope = FragmentScope::class, ignoreQualifier = true)
5838
class ContentScopeScriptsWebMessagingPlugin @Inject constructor(
59-
private val handlers: PluginPoint<WebViewCompatContentScopeJsMessageHandlersPlugin>,
60-
private val globalHandlers: PluginPoint<GlobalContentScopeJsMessageHandlersPlugin>,
61-
private val webViewCompatContentScopeScripts: WebViewCompatContentScopeScripts,
62-
private val webViewCompatWrapper: WebViewCompatWrapper,
63-
private val dispatcherProvider: DispatcherProvider,
64-
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
65-
) : WebMessagingPlugin {
66-
67-
private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build()
68-
69-
override val context: String = "contentScopeScripts"
70-
private val allowedDomains: Set<String> = setOf("*")
71-
72-
private var globalReplyProxy: JavaScriptReplyProxy? = null
73-
74-
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
75-
internal fun process(
76-
message: String,
77-
jsMessageCallback: WebViewCompatMessageCallback,
78-
replyProxy: JavaScriptReplyProxy,
79-
) {
80-
try {
81-
val adapter = moshi.adapter(JsMessage::class.java)
82-
val jsMessage = adapter.fromJson(message)
83-
84-
jsMessage?.let {
85-
if (context == jsMessage.context) {
86-
// Setup reply proxy so we can send subscription events
87-
if (jsMessage.featureName == "messaging" && jsMessage.method == "initialPing") {
88-
globalReplyProxy = replyProxy
89-
}
90-
91-
// Process global handlers first (always processed regardless of feature handlers)
92-
globalHandlers.getPlugins()
93-
.map { it.getGlobalJsMessageHandler() }
94-
.filter { it.method == jsMessage.method }
95-
.forEach { handler ->
96-
handler.process(jsMessage)?.let { processResult ->
97-
when (processResult) {
98-
is SendToConsumer -> {
99-
sendToConsumer(jsMessageCallback, jsMessage, replyProxy)
100-
}
101-
is SendResponse -> {
102-
onResponse(jsMessage, replyProxy)
103-
}
104-
}
105-
}
106-
}
107-
108-
// Process with feature handlers
109-
handlers.getPlugins().map { it.getJsMessageHandler() }.firstOrNull {
110-
it.methods.contains(jsMessage.method) && it.featureName == jsMessage.featureName
111-
}?.process(jsMessage)?.let { processResult ->
112-
when (processResult) {
113-
is SendToConsumer -> {
114-
sendToConsumer(jsMessageCallback, jsMessage, replyProxy)
115-
}
116-
is SendResponse -> {
117-
onResponse(jsMessage, replyProxy)
118-
}
119-
}
120-
}
121-
}
122-
}
123-
} catch (e: Exception) {
124-
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()
12552
}
126-
}
127-
128-
private fun onResponse(
129-
jsMessage: JsMessage,
130-
replyProxy: JavaScriptReplyProxy,
131-
) {
132-
val callbackData = JsCallbackData(
133-
id = jsMessage.id ?: "",
134-
params = jsMessage.params,
135-
featureName = jsMessage.featureName,
136-
method = jsMessage.method,
137-
)
138-
onResponse(callbackData, replyProxy)
139-
}
14053

141-
private fun sendToConsumer(
142-
jsMessageCallback: WebViewCompatMessageCallback,
143-
jsMessage: JsMessage,
144-
replyProxy: JavaScriptReplyProxy,
145-
) {
146-
jsMessageCallback.process(
147-
context = context,
148-
featureName = jsMessage.featureName,
149-
method = jsMessage.method,
150-
id = jsMessage.id ?: "",
151-
data = jsMessage.params,
152-
onResponse = { response: JSONObject ->
153-
val callbackData = JsCallbackData(
154-
id = jsMessage.id ?: "",
155-
params = response,
156-
featureName = jsMessage.featureName,
157-
method = jsMessage.method,
158-
)
159-
onResponse(callbackData, replyProxy)
160-
},
161-
)
162-
}
163-
164-
override fun register(
165-
jsMessageCallback: WebViewCompatMessageCallback,
166-
webView: WebView,
167-
) {
168-
appCoroutineScope.launch {
169-
if (!webViewCompatContentScopeScripts.isEnabled()) {
170-
return@launch
171-
}
172-
173-
runCatching {
174-
return@runCatching webViewCompatWrapper.addWebMessageListener(
175-
webView,
176-
JS_OBJECT_NAME,
177-
allowedDomains,
178-
) { _, message, _, _, replyProxy ->
179-
process(
180-
message.data ?: "",
181-
jsMessageCallback,
182-
replyProxy,
183-
)
184-
}
185-
}.getOrElse { exception ->
186-
logcat(ERROR) { "Error adding WebMessageListener for contentScopeAdsjs: ${exception.asLog()}" }
187-
}
188-
}
189-
}
190-
191-
override fun unregister(
192-
webView: WebView,
193-
) {
194-
appCoroutineScope.launch {
195-
if (!webViewCompatContentScopeScripts.isEnabled()) return@launch
196-
runCatching {
197-
return@runCatching webViewCompatWrapper.removeWebMessageListener(webView, JS_OBJECT_NAME)
198-
}.getOrElse { exception ->
199-
logcat(ERROR) {
200-
"Error removing WebMessageListener for contentScopeAdsjs: ${exception.asLog()}"
201-
}
202-
}
54+
override fun getMessageHandlers(): List<WebViewCompatMessageHandler> {
55+
return handlers.getPlugins().map { it.getJsMessageHandler() }
20356
}
204-
}
205-
206-
@SuppressLint("RequiresFeature")
207-
private fun onResponse(response: JsCallbackData, replyProxy: JavaScriptReplyProxy) {
208-
runCatching {
209-
val responseWithId = JSONObject().apply {
210-
put("id", response.id)
211-
put("result", response.params)
212-
put("featureName", response.featureName)
213-
put("context", context)
214-
}
215-
appCoroutineScope.launch(dispatcherProvider.main()) {
216-
replyProxy.postMessage(responseWithId.toString())
217-
}
218-
}
219-
}
220-
221-
@SuppressLint("RequiresFeature")
222-
override fun postMessage(subscriptionEventData: SubscriptionEventData) {
223-
runCatching {
224-
appCoroutineScope.launch {
225-
if (!webViewCompatContentScopeScripts.isEnabled()) {
226-
return@launch
227-
}
228-
229-
val subscriptionEvent = SubscriptionEvent(
230-
context = context,
231-
featureName = subscriptionEventData.featureName,
232-
subscriptionName = subscriptionEventData.subscriptionName,
233-
params = subscriptionEventData.params,
234-
).let {
235-
moshi.adapter(SubscriptionEvent::class.java).toJson(it)
236-
}
23757

238-
withContext(dispatcherProvider.main()) {
239-
globalReplyProxy?.postMessage(subscriptionEvent)
240-
}
241-
}
58+
override fun getGlobalMessageHandler(): List<GlobalJsMessageHandler> {
59+
return globalHandlers.getPlugins().map { it.getGlobalJsMessageHandler() }
24260
}
243-
}
244-
}
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
@@ -32,3 +32,42 @@ interface WebMessagingPlugin {
3232

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

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

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)