Skip to content

Commit f2a8e66

Browse files
committed
Add support to receive messages
1 parent bfcbc5b commit f2a8e66

File tree

17 files changed

+899
-19
lines changed

17 files changed

+899
-19
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
@@ -39,6 +39,7 @@ import androidx.core.net.toUri
3939
import androidx.test.annotation.UiThreadTest
4040
import androidx.test.filters.SdkSuppress
4141
import androidx.test.platform.app.InstrumentationRegistry
42+
import androidx.webkit.WebViewCompat.WebMessageListener
4243
import com.duckduckgo.adclick.api.AdClickManager
4344
import com.duckduckgo.anrs.api.CrashLogger
4445
import com.duckduckgo.anrs.api.CrashLogger.Crash
@@ -85,6 +86,8 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.Unavailab
8586
import com.duckduckgo.feature.toggles.api.Toggle
8687
import com.duckduckgo.history.api.NavigationHistory
8788
import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin
89+
import com.duckduckgo.js.messaging.api.JsMessageCallback
90+
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
8891
import com.duckduckgo.privacy.config.api.AmpLinks
8992
import com.duckduckgo.subscriptions.api.Subscriptions
9093
import com.duckduckgo.user.agent.api.ClientBrandHintProvider
@@ -163,6 +166,7 @@ class BrowserWebViewClientTest {
163166
private val mockAndroidBrowserConfigFeature: AndroidBrowserConfigFeature = mock()
164167
private val mockContentScopeExperiments: ContentScopeExperiments = mock()
165168
private val fakeAddDocumentStartJavaScriptPlugins = FakeAddDocumentStartJavaScriptPluginPoint()
169+
private val fakeMessagingPlugins = FakeWebMessagingPluginPoint()
166170
private val mockAndroidFeaturesHeaderPlugin = AndroidFeaturesHeaderPlugin(
167171
mockDuckDuckGoUrlDetector,
168172
mockCustomHeaderGracePeriodChecker,
@@ -212,6 +216,7 @@ class BrowserWebViewClientTest {
212216
mockDuckChat,
213217
mockContentScopeExperiments,
214218
fakeAddDocumentStartJavaScriptPlugins,
219+
fakeMessagingPlugins,
215220
)
216221
testee.webViewClientListener = listener
217222
whenever(webResourceRequest.url).thenReturn(Uri.EMPTY)
@@ -345,12 +350,33 @@ class BrowserWebViewClientTest {
345350

346351
@UiThreadTest
347352
@Test
348-
fun whenTriggerJsInitThenInjectJsCode() {
353+
fun whenConfigureWebViewThenInjectJsCode() {
349354
assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.plugin.countInitted)
350-
testee.configureWebView(DuckDuckGoWebView(context))
355+
val mockCallback = mock<JsMessageCallback>()
356+
testee.configureWebView(DuckDuckGoWebView(context), mockCallback)
351357
assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.plugin.countInitted)
352358
}
353359

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

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

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

@@ -3853,8 +3870,13 @@ class BrowserTabFragment :
38533870

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

38603882
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
@@ -80,6 +80,8 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On
8080
import com.duckduckgo.duckplayer.impl.DUCK_PLAYER_OPEN_IN_YOUTUBE_PATH
8181
import com.duckduckgo.history.api.NavigationHistory
8282
import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin
83+
import com.duckduckgo.js.messaging.api.JsMessageCallback
84+
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
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,12 +468,22 @@ 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.addDocumentStartJavaScript(activeExperiments, webView)
473476
}
477+
478+
webMessagingPlugins.getPlugins().forEach { plugin ->
479+
plugin.register(callback) { objectName, allowedOriginRules, webMessageListener ->
480+
webView.safeAddWebMessageListener(
481+
objectName,
482+
allowedOriginRules,
483+
webMessageListener,
484+
)
485+
}
486+
}
474487
}
475488
}
476489

@@ -757,6 +770,14 @@ class BrowserWebViewClient @Inject constructor(
757770
fun addExemptedMaliciousSite(url: Uri, feed: Feed) {
758771
requestInterceptor.addExemptedMaliciousSite(url, feed)
759772
}
773+
774+
suspend fun destroy(webView: DuckDuckGoWebView) {
775+
webMessagingPlugins.getPlugins().forEach { plugin ->
776+
plugin.unregister { objectName ->
777+
webView.safeRemoveWebMessageListener(objectName)
778+
}
779+
}
780+
}
760781
}
761782

762783
enum class WebViewPixelName(override val pixelName: String) : Pixel.PixelName {

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

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -430,25 +430,42 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
430430

431431
@SuppressLint("RequiresFeature", "AddWebMessageListenerUsage")
432432
suspend fun safeAddWebMessageListener(
433-
webViewCapabilityChecker: WebViewCapabilityChecker,
434433
jsObjectName: String,
435434
allowedOriginRules: Set<String>,
436435
listener: WebMessageListener,
437-
): Boolean = runCatching {
438-
if (webViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener) && !isDestroyed) {
439-
WebViewCompat.addWebMessageListener(
440-
this,
441-
jsObjectName,
442-
allowedOriginRules,
443-
listener,
444-
)
445-
true
446-
} else {
447-
false
436+
) = runCatching {
437+
if (!isDestroyed) {
438+
if (::dispatcherProvider.isInitialized) {
439+
withContext(dispatcherProvider.main()) {
440+
WebViewCompat.addWebMessageListener(
441+
this@DuckDuckGoWebView,
442+
jsObjectName,
443+
allowedOriginRules,
444+
listener,
445+
)
446+
}
447+
}
448448
}
449449
}.getOrElse { exception ->
450450
logcat(ERROR) { "Error adding WebMessageListener: $jsObjectName: ${exception.asLog()}" }
451-
false
451+
}
452+
453+
@SuppressLint("RequiresFeature", "RemoveWebMessageListenerUsage")
454+
suspend fun safeRemoveWebMessageListener(
455+
jsObjectName: String,
456+
) = runCatching {
457+
if (!isDestroyed) {
458+
if (::dispatcherProvider.isInitialized) {
459+
withContext(dispatcherProvider.main()) {
460+
WebViewCompat.removeWebMessageListener(
461+
this@DuckDuckGoWebView,
462+
jsObjectName,
463+
)
464+
}
465+
}
466+
}
467+
}.getOrElse { exception ->
468+
logcat(ERROR) { "Error removing WebMessageListener: $jsObjectName: ${exception.asLog()}" }
452469
}
453470

454471
@SuppressLint("RequiresFeature")

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.duckduckgo.app.browser
1818

1919
import android.annotation.SuppressLint
20+
import android.webkit.WebView
2021
import androidx.webkit.ScriptHandler
2122
import androidx.webkit.WebViewCompat
2223
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker
@@ -63,4 +64,20 @@ class RealWebViewCompatWrapper @Inject constructor(
6364
null
6465
}
6566
}
67+
68+
override fun removeWebMessageListener(webView: WebView, jsObjectName: String) {
69+
WebViewCompat.removeWebMessageListener(
70+
webView,
71+
jsObjectName,
72+
)
73+
}
74+
75+
override fun addWebMessageListener(
76+
webView: WebView,
77+
jsObjectName: String,
78+
allowedOriginRules: Set<String>,
79+
listener: WebViewCompat.WebMessageListener,
80+
) {
81+
return WebViewCompat.addWebMessageListener(webView, jsObjectName, allowedOriginRules, listener)
82+
}
6683
}
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.di.scopes.AppScope
21+
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
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+
}

0 commit comments

Comments
 (0)