Skip to content

Commit d22d4aa

Browse files
committed
Add delegate to simplify creating implementations of WebMessagingPlugin
1 parent 9c5603e commit d22d4aa

File tree

7 files changed

+344
-238
lines changed

7 files changed

+344
-238
lines changed

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

Lines changed: 23 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -16,237 +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.app.di.AppCoroutineScope
26-
import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper
2719
import com.duckduckgo.common.utils.plugins.PluginPoint
2820
import com.duckduckgo.contentscopescripts.api.WebViewCompatContentScopeJsMessageHandlersPlugin
2921
import com.duckduckgo.contentscopescripts.impl.WebViewCompatContentScopeScripts
3022
import com.duckduckgo.di.scopes.FragmentScope
31-
import com.duckduckgo.js.messaging.api.JsCallbackData
32-
import com.duckduckgo.js.messaging.api.JsMessage
33-
import com.duckduckgo.js.messaging.api.ProcessResult.SendResponse
34-
import com.duckduckgo.js.messaging.api.ProcessResult.SendToConsumer
35-
import com.duckduckgo.js.messaging.api.SubscriptionEvent
36-
import com.duckduckgo.js.messaging.api.SubscriptionEventData
23+
import com.duckduckgo.js.messaging.api.GlobalJsMessageHandler
3724
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
38-
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
3928
import com.squareup.anvil.annotations.ContributesBinding
4029
import com.squareup.anvil.annotations.ContributesMultibinding
41-
import com.squareup.moshi.Moshi
4230
import dagger.SingleInstanceIn
43-
import kotlinx.coroutines.launch
44-
import logcat.LogPriority.ERROR
45-
import logcat.asLog
46-
import logcat.logcat
47-
import org.json.JSONObject
4831
import javax.inject.Inject
4932
import javax.inject.Named
5033

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

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

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