Skip to content

Commit 12abeae

Browse files
committed
Add support to receive messages
1 parent fd8e009 commit 12abeae

File tree

19 files changed

+885
-10
lines changed

19 files changed

+885
-10
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import androidx.test.annotation.UiThreadTest
4040
import androidx.test.filters.SdkSuppress
4141
import androidx.test.platform.app.InstrumentationRegistry
4242
import androidx.webkit.ScriptHandler
43+
import androidx.webkit.WebViewCompat.WebMessageListener
4344
import com.duckduckgo.adclick.api.AdClickManager
4445
import com.duckduckgo.anrs.api.CrashLogger
4546
import com.duckduckgo.anrs.api.CrashLogger.Crash
@@ -75,6 +76,7 @@ import com.duckduckgo.common.utils.CurrentTimeProvider
7576
import com.duckduckgo.common.utils.device.DeviceInfo
7677
import com.duckduckgo.common.utils.plugins.PluginPoint
7778
import com.duckduckgo.contentscopescripts.api.AddDocumentStartJavaScriptPlugin
79+
import com.duckduckgo.contentscopescripts.api.WebMessagingPlugin
7880
import com.duckduckgo.contentscopescripts.api.contentscopeExperiments.ContentScopeExperiments
7981
import com.duckduckgo.cookies.api.CookieManagerProvider
8082
import com.duckduckgo.duckchat.api.DuckChat
@@ -86,6 +88,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On
8688
import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.Unavailable
8789
import com.duckduckgo.feature.toggles.api.Toggle
8890
import com.duckduckgo.history.api.NavigationHistory
91+
import com.duckduckgo.js.messaging.api.JsMessageCallback
8992
import com.duckduckgo.privacy.config.api.AmpLinks
9093
import com.duckduckgo.subscriptions.api.Subscriptions
9194
import com.duckduckgo.user.agent.api.ClientBrandHintProvider
@@ -164,6 +167,7 @@ class BrowserWebViewClientTest {
164167
private val mockAndroidBrowserConfigFeature: AndroidBrowserConfigFeature = mock()
165168
private val mockContentScopeExperiments: ContentScopeExperiments = mock()
166169
private val fakeAddDocumentStartJavaScriptPlugins = FakeAddDocumentStartJavaScriptPluginPoint()
170+
private val fakeMessagingPlugins = FakeWebMessagingPluginPoint()
167171
private val mockAndroidFeaturesHeaderPlugin = AndroidFeaturesHeaderPlugin(
168172
mockDuckDuckGoUrlDetector,
169173
mockCustomHeaderGracePeriodChecker,
@@ -213,6 +217,7 @@ class BrowserWebViewClientTest {
213217
mockDuckChat,
214218
mockContentScopeExperiments,
215219
fakeAddDocumentStartJavaScriptPlugins,
220+
fakeMessagingPlugins,
216221
)
217222
testee.webViewClientListener = listener
218223
whenever(webResourceRequest.url).thenReturn(Uri.EMPTY)
@@ -346,12 +351,33 @@ class BrowserWebViewClientTest {
346351

347352
@UiThreadTest
348353
@Test
349-
fun whenTriggerJsInitThenInjectJsCode() {
354+
fun whenConfigureWebViewThenInjectJsCode() {
350355
assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.plugin.countInitted)
351-
testee.configureWebView(DuckDuckGoWebView(context))
356+
val mockCallback = mock<JsMessageCallback>()
357+
testee.configureWebView(DuckDuckGoWebView(context), mockCallback)
352358
assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.plugin.countInitted)
353359
}
354360

361+
@UiThreadTest
362+
@Test
363+
fun whenConfigureWebViewThenAddWebMessageListener() {
364+
assertFalse(fakeMessagingPlugins.plugin.registered)
365+
val mockCallback = mock<JsMessageCallback>()
366+
testee.configureWebView(DuckDuckGoWebView(context), mockCallback)
367+
assertTrue(fakeMessagingPlugins.plugin.registered)
368+
}
369+
370+
@UiThreadTest
371+
@Test
372+
fun whenDestroyThenRemoveWebMessageListener() = runTest {
373+
val mockCallback = mock<JsMessageCallback>()
374+
val webView = DuckDuckGoWebView(context)
375+
testee.configureWebView(webView, mockCallback)
376+
assertTrue(fakeMessagingPlugins.plugin.registered)
377+
testee.destroy(webView)
378+
assertFalse(fakeMessagingPlugins.plugin.registered)
379+
}
380+
355381
@UiThreadTest
356382
@Test
357383
fun whenOnReceivedHttpAuthRequestThenListenerNotified() {
@@ -1309,4 +1335,28 @@ class BrowserWebViewClientTest {
13091335

13101336
override fun getPlugins() = listOf(plugin)
13111337
}
1338+
1339+
class FakeWebMessagingPlugin : WebMessagingPlugin {
1340+
var registered = false
1341+
private set
1342+
1343+
override suspend fun unregister(unregisterer: suspend (objectName: String) -> Boolean) {
1344+
registered = false
1345+
}
1346+
1347+
override suspend fun register(
1348+
jsMessageCallback: JsMessageCallback?,
1349+
registerer: suspend (objectName: String, allowedOriginRules: Set<String>, webMessageListener: WebMessageListener) -> Boolean,
1350+
) {
1351+
registered = true
1352+
}
1353+
}
1354+
1355+
class FakeWebMessagingPluginPoint : PluginPoint<WebMessagingPlugin> {
1356+
val plugin = FakeWebMessagingPlugin()
1357+
1358+
override fun getPlugins(): Collection<WebMessagingPlugin> {
1359+
return listOf(plugin)
1360+
}
1361+
}
13121362
}

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 {

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import androidx.core.view.NestedScrollingChild3
3434
import androidx.core.view.NestedScrollingChildHelper
3535
import androidx.core.view.ViewCompat
3636
import androidx.webkit.ScriptHandler
37-
import androidx.webkit.WebViewCompat
3837
import androidx.webkit.WebViewCompat.WebMessageListener
3938
import com.duckduckgo.anvil.annotations.InjectWith
4039
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker
@@ -437,7 +436,7 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
437436
listener: WebMessageListener,
438437
): Boolean = runCatching {
439438
if (webViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener) && !isDestroyed) {
440-
WebViewCompat.addWebMessageListener(
439+
webViewCompatWrapper.addWebMessageListener(
441440
this,
442441
jsObjectName,
443442
allowedOriginRules,
@@ -457,7 +456,7 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
457456
jsObjectName: String,
458457
): Boolean = runCatching {
459458
if (webViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener) && !isDestroyed) {
460-
WebViewCompat.removeWebMessageListener(
459+
webViewCompatWrapper.removeWebMessageListener(
461460
this,
462461
jsObjectName,
463462
)
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,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+
}

0 commit comments

Comments
 (0)