Skip to content

Commit 650cfb4

Browse files
committed
Add support to receive messages
1 parent 6d226d6 commit 650cfb4

File tree

17 files changed

+831
-5
lines changed

17 files changed

+831
-5
lines changed

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3021,7 +3021,24 @@ class BrowserTabFragment :
30213021
webView?.let {
30223022
it.isSafeWebViewEnabled = safeWebViewFeature.self().isEnabled()
30233023
it.webViewClient = webViewClient
3024-
webViewClient.configureWebView(it)
3024+
lifecycleScope.launch(dispatchers.main()) {
3025+
webViewClient.configureWebView(
3026+
it,
3027+
object : JsMessageCallback() {
3028+
override fun process(
3029+
featureName: String,
3030+
method: String,
3031+
id: String?,
3032+
data: JSONObject?,
3033+
) {
3034+
viewModel.processJsCallbackMessage(featureName, method, id, data, isActiveCustomTab()) {
3035+
it.url
3036+
}
3037+
}
3038+
},
3039+
)
3040+
}
3041+
30253042
it.webChromeClient = webChromeClient
30263043
it.clearSslPreferences()
30273044

@@ -3852,8 +3869,13 @@ class BrowserTabFragment :
38523869

38533870
private fun destroyWebView() {
38543871
if (::webViewContainer.isInitialized) webViewContainer.removeAllViews()
3855-
webView?.destroy()
3856-
webView = null
3872+
appCoroutineScope.launch(dispatchers.main()) {
3873+
webView?.let {
3874+
webViewClient.destroy(it)
3875+
it.destroy()
3876+
}
3877+
webView = null
3878+
}
38573879
}
38583880

38593881
private fun convertBlobToDataUri(blob: Command.ConvertBlobToDataUri) {

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import com.duckduckgo.common.utils.CurrentTimeProvider
7171
import com.duckduckgo.common.utils.DispatcherProvider
7272
import com.duckduckgo.common.utils.plugins.PluginPoint
7373
import com.duckduckgo.contentscopescripts.api.AddDocumentStartJavaScriptPlugin
74+
import com.duckduckgo.contentscopescripts.api.WebMessagingPlugin
7475
import com.duckduckgo.contentscopescripts.api.contentscopeExperiments.ContentScopeExperiments
7576
import com.duckduckgo.cookies.api.CookieManagerProvider
7677
import com.duckduckgo.duckchat.api.DuckChat
@@ -80,6 +81,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
8081
import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On
8182
import com.duckduckgo.duckplayer.impl.DUCK_PLAYER_OPEN_IN_YOUTUBE_PATH
8283
import com.duckduckgo.history.api.NavigationHistory
84+
import com.duckduckgo.js.messaging.api.JsMessageCallback
8385
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
8486
import com.duckduckgo.privacy.config.api.AmpLinks
8587
import com.duckduckgo.subscriptions.api.Subscriptions
@@ -128,6 +130,7 @@ class BrowserWebViewClient @Inject constructor(
128130
private val duckChat: DuckChat,
129131
private val contentScopeExperiments: ContentScopeExperiments,
130132
private val addDocumentStartJavascriptPlugins: PluginPoint<AddDocumentStartJavaScriptPlugin>,
133+
private val webMessagingPlugins: PluginPoint<WebMessagingPlugin>,
131134
) : WebViewClient() {
132135

133136
var webViewClientListener: WebViewClientListener? = null
@@ -465,14 +468,24 @@ class BrowserWebViewClient @Inject constructor(
465468
webView.settings.mediaPlaybackRequiresUserGesture = mediaPlayback.doesMediaPlaybackRequireUserGestureForUrl(url)
466469
}
467470

468-
fun configureWebView(webView: DuckDuckGoWebView) {
471+
fun configureWebView(webView: DuckDuckGoWebView, callback: JsMessageCallback) {
469472
appCoroutineScope.launch {
470473
val activeExperiments = contentScopeExperiments.getActiveExperiments()
471474
addDocumentStartJavascriptPlugins.getPlugins().forEach { plugin ->
472475
plugin.configureAddDocumentStartJavaScript(activeExperiments) { scriptString, allowedOrigins ->
473476
webView.safeAddDocumentStartJavaScript(scriptString, allowedOrigins)
474477
}
475478
}
479+
480+
webMessagingPlugins.getPlugins().forEach { plugin ->
481+
plugin.register(callback) { objectName, allowedOriginRules, webMessageListener ->
482+
webView.safeAddWebMessageListener(
483+
objectName,
484+
allowedOriginRules,
485+
webMessageListener,
486+
)
487+
}
488+
}
476489
}
477490
}
478491

@@ -760,6 +773,14 @@ class BrowserWebViewClient @Inject constructor(
760773
fun addExemptedMaliciousSite(url: Uri, feed: Feed) {
761774
requestInterceptor.addExemptedMaliciousSite(url, feed)
762775
}
776+
777+
suspend fun destroy(webView: DuckDuckGoWebView) {
778+
webMessagingPlugins.getPlugins().forEach { plugin ->
779+
plugin.unregister { objectName ->
780+
webView.safeRemoveWebMessageListener(objectName)
781+
}
782+
}
783+
}
763784
}
764785

765786
enum class WebViewPixelName(override val pixelName: String) : Pixel.PixelName {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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.app.plugins
18+
19+
import com.duckduckgo.anvil.annotations.ContributesPluginPoint
20+
import com.duckduckgo.contentscopescripts.api.WebMessagingPlugin
21+
import com.duckduckgo.di.scopes.AppScope
22+
23+
@ContributesPluginPoint(
24+
scope = AppScope::class,
25+
boundType = WebMessagingPlugin::class,
26+
)
27+
@Suppress("unused")
28+
interface UnusedWebMessagingPluginPoint
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.duckduckgo.app.browser
2+
3+
import androidx.test.ext.junit.runners.AndroidJUnit4
4+
import androidx.test.platform.app.InstrumentationRegistry
5+
import androidx.webkit.WebViewCompat.WebMessageListener
6+
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker
7+
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability
8+
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability.DocumentStartJavaScript
9+
import com.duckduckgo.contentscopescripts.impl.WebViewCompatWrapper
10+
import kotlinx.coroutines.test.runTest
11+
import org.junit.Before
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
import org.mockito.Mockito.mock
15+
import org.mockito.Mockito.verify
16+
import org.mockito.kotlin.never
17+
import org.mockito.kotlin.whenever
18+
19+
@RunWith(AndroidJUnit4::class)
20+
class DuckDuckGoWebViewTest {
21+
22+
val testee: DuckDuckGoWebView = DuckDuckGoWebView(InstrumentationRegistry.getInstrumentation().targetContext)
23+
24+
private val mockWebViewCapabilityChecker: WebViewCapabilityChecker = mock()
25+
private val mockWebViewCompatWrapper: WebViewCompatWrapper = mock()
26+
private val mockWebMessageListener: WebMessageListener = mock()
27+
28+
@Before
29+
fun setUp() {
30+
testee.webViewCompatWrapper = mockWebViewCompatWrapper
31+
testee.webViewCapabilityChecker = mockWebViewCapabilityChecker
32+
}
33+
34+
@Test
35+
fun whenSafeAddDocumentStartJavaScriptWithFeatureEnabledThenAddScript() = runTest {
36+
whenever(mockWebViewCapabilityChecker.isSupported(DocumentStartJavaScript)).thenReturn(true)
37+
38+
testee.safeAddDocumentStartJavaScript("script", setOf("*"))
39+
40+
verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(testee, "script", setOf("*"))
41+
}
42+
43+
@Test
44+
fun whenSafeAddDocumentStartJavaScriptWithFeatureDisabledThenDoNotAddScript() = runTest {
45+
whenever(mockWebViewCapabilityChecker.isSupported(DocumentStartJavaScript)).thenReturn(false)
46+
47+
testee.safeAddDocumentStartJavaScript("script", setOf("*"))
48+
49+
verify(mockWebViewCompatWrapper, never()).addDocumentStartJavaScript(testee, "script", setOf("*"))
50+
}
51+
52+
@Test
53+
fun whenSafeAddWebMessageListenerWithFeatureEnabledThenAddListener() = runTest {
54+
whenever(mockWebViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener)).thenReturn(true)
55+
56+
testee.safeAddWebMessageListener("test", setOf("*"), mockWebMessageListener)
57+
verify(mockWebViewCompatWrapper)
58+
.addWebMessageListener(testee, "test", setOf("*"), mockWebMessageListener)
59+
}
60+
61+
@Test
62+
fun whenSafeAddWebMessageListenerWithFeatureDisabledThenDoNotAddListener() = runTest {
63+
whenever(mockWebViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener)).thenReturn(false)
64+
65+
testee.safeAddWebMessageListener("test", setOf("*"), mockWebMessageListener)
66+
verify(mockWebViewCompatWrapper, never())
67+
.addWebMessageListener(testee, "test", setOf("*"), mockWebMessageListener)
68+
}
69+
70+
@Test
71+
fun whenSafeRemoveWebMessageListenerWithFeatureEnabledThenRemoveListener() = runTest {
72+
whenever(mockWebViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener)).thenReturn(true)
73+
74+
testee.safeRemoveWebMessageListener("test")
75+
76+
verify(mockWebViewCompatWrapper).removeWebMessageListener(testee, "test")
77+
}
78+
79+
@Test
80+
fun whenSafeRemoveWebMessageListenerWithFeatureDisabledThenDoNotRemoveListener() = runTest {
81+
whenever(mockWebViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener)).thenReturn(false)
82+
83+
testee.safeRemoveWebMessageListener("test")
84+
verify(mockWebViewCompatWrapper, never()).removeWebMessageListener(testee, "test")
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 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+
* @param jsMessageCallback An optional callback to handle the result of the message processing.
48+
*/
49+
fun process(
50+
jsMessage: JsMessage,
51+
jsMessageCallback: JsMessageCallback,
52+
)
53+
54+
/**
55+
* Method this handler can process.
56+
*/
57+
val method: String
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 androidx.webkit.WebViewCompat.WebMessageListener
20+
import com.duckduckgo.js.messaging.api.JsMessageCallback
21+
22+
interface WebMessagingPlugin {
23+
suspend fun register(
24+
jsMessageCallback: JsMessageCallback?,
25+
registerer: suspend (objectName: String, allowedOriginRules: Set<String>, webMessageListener: WebMessageListener) -> Boolean,
26+
)
27+
28+
suspend fun unregister(
29+
unregisterer: suspend (objectName: String) -> Boolean,
30+
)
31+
}
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.WebViewCompatMessageHandler
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 WebViewCompatContentScopeJsMessageHandlersPlugin {
25+
/**
26+
* @return a [WebViewCompatMessageHandler] that will be used to handle the JS messages
27+
*/
28+
fun getJsMessageHandler(): WebViewCompatMessageHandler
29+
}
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

0 commit comments

Comments
 (0)