Skip to content

Commit cc13d8c

Browse files
committed
Add support to receive messages
1 parent 9ec7474 commit cc13d8c

File tree

9 files changed

+375
-1
lines changed

9 files changed

+375
-1
lines changed

app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ import com.duckduckgo.duckchat.api.inputscreen.InputScreenActivityResultCodes
295295
import com.duckduckgo.duckchat.api.inputscreen.InputScreenActivityResultParams
296296
import com.duckduckgo.duckplayer.api.DuckPlayer
297297
import com.duckduckgo.duckplayer.api.DuckPlayerSettingsNoParams
298+
import com.duckduckgo.js.messaging.api.AdsjsMessaging
298299
import com.duckduckgo.js.messaging.api.JsCallbackData
299300
import com.duckduckgo.js.messaging.api.JsMessageCallback
300301
import com.duckduckgo.js.messaging.api.JsMessaging
@@ -511,6 +512,10 @@ class BrowserTabFragment :
511512
@Named("DuckPlayer")
512513
lateinit var duckPlayerScripts: JsMessaging
513514

515+
@Inject
516+
@Named("AdsjsContentScopeScripts")
517+
lateinit var adsJsContentScopeScripts: AdsjsMessaging
518+
514519
@Inject
515520
lateinit var webContentDebugging: WebContentDebugging
516521

@@ -3006,7 +3011,25 @@ class BrowserTabFragment :
30063011
webView?.let {
30073012
it.isSafeWebViewEnabled = safeWebViewFeature.self().isEnabled()
30083013
it.webViewClient = webViewClient
3009-
webViewClient.triggerJSInit(it)
3014+
lifecycleScope.launch(dispatchers.main()) {
3015+
webViewClient.triggerJSInit(it)
3016+
adsJsContentScopeScripts.register(
3017+
it,
3018+
object : JsMessageCallback() {
3019+
override fun process(
3020+
featureName: String,
3021+
method: String,
3022+
id: String?,
3023+
data: JSONObject?,
3024+
) {
3025+
viewModel.processJsCallbackMessage(featureName, method, id, data, isActiveCustomTab()) {
3026+
it.url
3027+
}
3028+
}
3029+
},
3030+
)
3031+
}
3032+
30103033
it.webChromeClient = webChromeClient
30113034
it.clearSslPreferences()
30123035

content-scope-scripts/content-scope-scripts-api/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ apply from: "$rootProject.projectDir/gradle/android-library.gradle"
2323

2424
dependencies {
2525
implementation AndroidX.core.ktx
26+
implementation AndroidX.webkit
2627
implementation project(':feature-toggles-api')
2728
implementation project(':js-messaging-api')
2829
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.contentscopescripts.api
18+
19+
import com.duckduckgo.js.messaging.api.AdsjsMessageHandler
20+
21+
/**
22+
* Implement this interface and contribute it as a multibinding to manage JS Messages that are sent to C-S-S
23+
*/
24+
interface AdsjsContentScopeJsMessageHandlersPlugin {
25+
/**
26+
* @return a [AdsjsMessageHandler] that will be used to handle the JS messages
27+
*/
28+
fun getJsMessageHandler(): AdsjsMessageHandler
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.contentscopescripts.api
18+
19+
import com.duckduckgo.js.messaging.api.JsMessage
20+
import com.duckduckgo.js.messaging.api.JsMessageCallback
21+
22+
/**
23+
* Plugin interface for global message handlers that should always be processed
24+
* regardless of whether a specific feature handler matches the message.
25+
* * Examples: addDebugFlag.
26+
*/
27+
interface GlobalContentScopeJsMessageHandlersPlugin {
28+
29+
/**
30+
* @return a [GlobalJsMessageHandler] that will be used to handle global messages
31+
*/
32+
fun getGlobalJsMessageHandler(): GlobalJsMessageHandler
33+
}
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.
42+
*/
43+
fun process(
44+
jsMessage: JsMessage,
45+
jsMessageCallback: JsMessageCallback,
46+
)
47+
48+
/**
49+
* Method this handler can process.
50+
*/
51+
val method: String
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2022 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.contentscopescripts.impl
18+
19+
import com.duckduckgo.anvil.annotations.ContributesPluginPoint
20+
import com.duckduckgo.contentscopescripts.api.AdsjsContentScopeJsMessageHandlersPlugin
21+
import com.duckduckgo.di.scopes.AppScope
22+
23+
@ContributesPluginPoint(
24+
scope = AppScope::class,
25+
boundType = AdsjsContentScopeJsMessageHandlersPlugin::class,
26+
)
27+
@Suppress("unused")
28+
interface AdsjsContentScopeJsMessageHandlersPluginPoint
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.contentscopescripts.impl
18+
19+
import com.duckduckgo.anvil.annotations.ContributesPluginPoint
20+
import com.duckduckgo.contentscopescripts.api.GlobalContentScopeJsMessageHandlersPlugin
21+
import com.duckduckgo.di.scopes.AppScope
22+
23+
@ContributesPluginPoint(
24+
scope = AppScope::class,
25+
boundType = GlobalContentScopeJsMessageHandlersPlugin::class,
26+
)
27+
@Suppress("unused")
28+
interface GlobalContentScopeJsMessageHandlersPluginPoint
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright (c) 2023 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.contentscopescripts.impl.messaging
18+
19+
import android.annotation.SuppressLint
20+
import android.webkit.WebView
21+
import androidx.webkit.WebViewCompat
22+
import androidx.webkit.WebViewFeature
23+
import com.duckduckgo.common.utils.plugins.PluginPoint
24+
import com.duckduckgo.contentscopescripts.api.AdsjsContentScopeJsMessageHandlersPlugin
25+
import com.duckduckgo.contentscopescripts.api.GlobalContentScopeJsMessageHandlersPlugin
26+
import com.duckduckgo.di.scopes.ActivityScope
27+
import com.duckduckgo.js.messaging.api.AdsjsMessaging
28+
import com.duckduckgo.js.messaging.api.JsMessage
29+
import com.duckduckgo.js.messaging.api.JsMessageCallback
30+
import com.squareup.anvil.annotations.ContributesBinding
31+
import com.squareup.moshi.Moshi
32+
import javax.inject.Inject
33+
import javax.inject.Named
34+
import logcat.LogPriority.ERROR
35+
import logcat.asLog
36+
import logcat.logcat
37+
38+
@ContributesBinding(ActivityScope::class)
39+
@Named("AdsjsContentScopeScripts")
40+
class AdsjsContentScopeMessaging @Inject constructor(
41+
private val handlers: PluginPoint<AdsjsContentScopeJsMessageHandlersPlugin>,
42+
private val globalHandlers: PluginPoint<GlobalContentScopeJsMessageHandlersPlugin>,
43+
) : AdsjsMessaging {
44+
45+
private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build()
46+
47+
private lateinit var webView: WebView
48+
49+
override val context: String = "contentScopeScripts"
50+
override val allowedDomains: Set<String> = setOf("*")
51+
52+
private fun process(
53+
message: String,
54+
jsMessageCallback: JsMessageCallback,
55+
) {
56+
try {
57+
val adapter = moshi.adapter(JsMessage::class.java)
58+
val jsMessage = adapter.fromJson(message)
59+
60+
jsMessage?.let {
61+
if (context == jsMessage.context) {
62+
// Process global handlers first (always processed regardless of feature handlers)
63+
globalHandlers.getPlugins()
64+
.map { it.getGlobalJsMessageHandler() }
65+
.filter { it.method == jsMessage.method }
66+
.forEach { handler ->
67+
handler.process(jsMessage, jsMessageCallback)
68+
}
69+
70+
// Process with feature handlers
71+
handlers.getPlugins().map { it.getJsMessageHandler() }.firstOrNull {
72+
it.methods.contains(jsMessage.method) && it.featureName == jsMessage.featureName
73+
}?.process(jsMessage, jsMessageCallback)
74+
}
75+
}
76+
} catch (e: Exception) {
77+
logcat(ERROR) { "Exception is ${e.asLog()}" }
78+
}
79+
}
80+
81+
// TODO: A/B this, don't register if the feature is not enabled
82+
@SuppressLint("AddWebMessageListenerUsage") // safeAddWebMessageListener belongs to app module
83+
override fun register(webView: WebView, jsMessageCallback: JsMessageCallback?) {
84+
if (jsMessageCallback == null) throw Exception("Callback cannot be null")
85+
this.webView = webView
86+
87+
runCatching {
88+
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
89+
WebViewCompat.addWebMessageListener(
90+
webView,
91+
"contentScopeAdsjs",
92+
allowedDomains,
93+
) { _, message, _, _, replyProxy ->
94+
process(
95+
message.data ?: "",
96+
jsMessageCallback,
97+
)
98+
}
99+
true
100+
} else {
101+
false
102+
}
103+
}.getOrElse { exception ->
104+
logcat(ERROR) { "Error adding WebMessageListener for contentScopeAdsjs: ${exception.asLog()}" }
105+
false
106+
}
107+
}
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.contentscopescripts.impl.messaging
18+
19+
import com.duckduckgo.contentscopescripts.api.GlobalContentScopeJsMessageHandlersPlugin
20+
import com.duckduckgo.contentscopescripts.api.GlobalJsMessageHandler
21+
import com.duckduckgo.di.scopes.AppScope
22+
import com.duckduckgo.js.messaging.api.JsMessage
23+
import com.duckduckgo.js.messaging.api.JsMessageCallback
24+
import com.squareup.anvil.annotations.ContributesMultibinding
25+
import javax.inject.Inject
26+
27+
@ContributesMultibinding(AppScope::class)
28+
class DebugFlagGlobalHandler @Inject constructor() : GlobalContentScopeJsMessageHandlersPlugin {
29+
30+
override fun getGlobalJsMessageHandler(): GlobalJsMessageHandler = object : GlobalJsMessageHandler {
31+
32+
override fun process(
33+
jsMessage: JsMessage,
34+
jsMessageCallback: JsMessageCallback,
35+
) {
36+
if (jsMessage.method == method) {
37+
jsMessageCallback.process(
38+
featureName = jsMessage.featureName,
39+
method = jsMessage.method,
40+
id = jsMessage.id,
41+
data = jsMessage.params,
42+
)
43+
}
44+
}
45+
46+
override val method: String = "addDebugFlag"
47+
}
48+
}

0 commit comments

Comments
 (0)