Skip to content

Commit 8f1ad21

Browse files
committed
Add support to addDocumentStartJavaScript
1 parent 48e6fbb commit 8f1ad21

File tree

24 files changed

+1183
-35
lines changed

24 files changed

+1183
-35
lines changed

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

Lines changed: 51 additions & 8 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.ScriptHandler
4243
import com.duckduckgo.adclick.api.AdClickManager
4344
import com.duckduckgo.anrs.api.CrashLogger
4445
import com.duckduckgo.anrs.api.CrashLogger.Crash
@@ -73,6 +74,7 @@ import com.duckduckgo.common.test.CoroutineTestRule
7374
import com.duckduckgo.common.utils.CurrentTimeProvider
7475
import com.duckduckgo.common.utils.device.DeviceInfo
7576
import com.duckduckgo.common.utils.plugins.PluginPoint
77+
import com.duckduckgo.contentscopescripts.api.AddDocumentStartJavaScriptPlugin
7678
import com.duckduckgo.contentscopescripts.api.contentscopeExperiments.ContentScopeExperiments
7779
import com.duckduckgo.cookies.api.CookieManagerProvider
7880
import com.duckduckgo.duckchat.api.DuckChat
@@ -112,6 +114,8 @@ import org.mockito.kotlin.verify
112114
import org.mockito.kotlin.verifyNoInteractions
113115
import org.mockito.kotlin.whenever
114116

117+
private val mockToggle: Toggle = mock()
118+
115119
class BrowserWebViewClientTest {
116120

117121
@get:Rule
@@ -158,6 +162,8 @@ class BrowserWebViewClientTest {
158162
private val openInNewTabFlow: MutableSharedFlow<OpenDuckPlayerInNewTab> = MutableSharedFlow()
159163
private val mockUriLoadedManager: UriLoadedManager = mock()
160164
private val mockAndroidBrowserConfigFeature: AndroidBrowserConfigFeature = mock()
165+
private val mockContentScopeExperiments: ContentScopeExperiments = mock()
166+
private val fakeAddDocumentStartJavaScriptPlugins = FakeAddDocumentStartJavaScriptPluginPoint()
161167
private val mockAndroidFeaturesHeaderPlugin = AndroidFeaturesHeaderPlugin(
162168
mockDuckDuckGoUrlDetector,
163169
mockCustomHeaderGracePeriodChecker,
@@ -166,15 +172,13 @@ class BrowserWebViewClientTest {
166172
mock(),
167173
)
168174
private val mockDuckChat: DuckChat = mock()
169-
private val mockContentScopeExperiments: ContentScopeExperiments = mock()
170175

171176
@UiThreadTest
172177
@Before
173178
fun setup() = runTest {
174179
webView = TestWebView(context)
175180
whenever(mockDuckPlayer.observeShouldOpenInNewTab()).thenReturn(openInNewTabFlow)
176-
val toggle: Toggle = mock()
177-
whenever(mockContentScopeExperiments.getActiveExperiments()).thenReturn(listOf(toggle))
181+
whenever(mockContentScopeExperiments.getActiveExperiments()).thenReturn(listOf(mockToggle))
178182
testee = BrowserWebViewClient(
179183
webViewHttpAuthStore,
180184
trustedCertificateStore,
@@ -208,6 +212,7 @@ class BrowserWebViewClientTest {
208212
mockAndroidFeaturesHeaderPlugin,
209213
mockDuckChat,
210214
mockContentScopeExperiments,
215+
fakeAddDocumentStartJavaScriptPlugins,
211216
)
212217
testee.webViewClientListener = listener
213218
whenever(webResourceRequest.url).thenReturn(Uri.EMPTY)
@@ -227,11 +232,8 @@ class BrowserWebViewClientTest {
227232
@UiThreadTest
228233
@Test
229234
fun whenOnPageStartedCalledThenListenerNotified() = runTest {
230-
val toggle: Toggle = mock()
231-
whenever(mockContentScopeExperiments.getActiveExperiments()).thenReturn(listOf(toggle))
232-
233235
testee.onPageStarted(webView, EXAMPLE_URL, null)
234-
verify(listener).pageStarted(any(), eq(listOf(toggle)))
236+
verify(listener).pageStarted(any(), eq(listOf(mockToggle)))
235237
}
236238

237239
@UiThreadTest
@@ -333,6 +335,23 @@ class BrowserWebViewClientTest {
333335
assertEquals(0, jsPlugins.plugin.countStarted)
334336
}
335337

338+
@UiThreadTest
339+
@Test
340+
fun whenOnPageStartedThenReturnActiveExperiments() {
341+
val captor = argumentCaptor<List<Toggle>>()
342+
testee.onPageStarted(webView, EXAMPLE_URL, null)
343+
verify(listener).pageStarted(any(), captor.capture())
344+
assertTrue(captor.firstValue.contains(mockToggle))
345+
}
346+
347+
@UiThreadTest
348+
@Test
349+
fun whenTriggerJsInitThenInjectJsCode() {
350+
assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.plugin.countInitted)
351+
testee.configureWebView(DuckDuckGoWebView(context))
352+
assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.plugin.countInitted)
353+
}
354+
336355
@UiThreadTest
337356
@Test
338357
fun whenOnReceivedHttpAuthRequestThenListenerNotified() {
@@ -1203,7 +1222,11 @@ class BrowserWebViewClientTest {
12031222
countStarted++
12041223
}
12051224

1206-
override fun onPageFinished(webView: WebView, url: String?, site: Site?) {
1225+
override fun onPageFinished(
1226+
webView: WebView,
1227+
url: String?,
1228+
site: Site?,
1229+
) {
12071230
countFinished++
12081231
}
12091232
}
@@ -1266,4 +1289,24 @@ class BrowserWebViewClientTest {
12661289
companion object {
12671290
const val EXAMPLE_URL = "https://example.com"
12681291
}
1292+
1293+
class FakeAddDocumentStartJavaScriptPlugin : AddDocumentStartJavaScriptPlugin {
1294+
1295+
var countInitted = 0
1296+
private set
1297+
1298+
override suspend fun configureAddDocumentStartJavaScript(
1299+
activeExperiments: List<Toggle>,
1300+
scriptInjector: suspend (scriptString: String, allowedOriginRules: Set<String>) -> ScriptHandler?,
1301+
) {
1302+
countInitted++
1303+
}
1304+
}
1305+
1306+
class FakeAddDocumentStartJavaScriptPluginPoint : PluginPoint<AddDocumentStartJavaScriptPlugin> {
1307+
1308+
val plugin = FakeAddDocumentStartJavaScriptPlugin()
1309+
1310+
override fun getPlugins() = listOf(plugin)
1311+
}
12691312
}

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

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

1919
import androidx.test.annotation.UiThreadTest
20+
import androidx.test.ext.junit.runners.AndroidJUnit4
2021
import androidx.test.platform.app.InstrumentationRegistry
22+
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker
23+
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability.DocumentStartJavaScript
24+
import com.duckduckgo.contentscopescripts.impl.WebViewCompatWrapper
25+
import kotlinx.coroutines.test.runTest
2126
import org.junit.Assert.assertFalse
27+
import org.junit.Before
2228
import org.junit.Test
29+
import org.junit.runner.RunWith
30+
import org.mockito.Mockito.mock
31+
import org.mockito.Mockito.verify
32+
import org.mockito.kotlin.never
33+
import org.mockito.kotlin.whenever
2334

35+
@RunWith(AndroidJUnit4::class)
2436
class DuckDuckGoWebViewTest {
2537

38+
private lateinit var testee: DuckDuckGoWebView
39+
private val mockWebViewCapabilityChecker: WebViewCapabilityChecker = mock()
40+
private val mockWebViewCompatWrapper: WebViewCompatWrapper = mock()
41+
42+
@Before
43+
@UiThreadTest
44+
fun setUp() {
45+
val context = InstrumentationRegistry.getInstrumentation().targetContext
46+
testee = DuckDuckGoWebView(context)
47+
testee.webViewCompatWrapper = mockWebViewCompatWrapper
48+
testee.webViewCapabilityChecker = mockWebViewCapabilityChecker
49+
}
50+
2651
@Test
2752
@UiThreadTest
2853
fun whenWebViewInitialisedThenSafeBrowsingDisabled() {
29-
val context = InstrumentationRegistry.getInstrumentation().targetContext
30-
val testee = DuckDuckGoWebView(context)
3154
assertFalse(testee.settings.safeBrowsingEnabled)
3255
}
56+
57+
@Test
58+
fun whenSafeAddDocumentStartJavaScriptWithFeatureEnabledThenAddScript() = runTest {
59+
whenever(mockWebViewCapabilityChecker.isSupported(DocumentStartJavaScript)).thenReturn(true)
60+
61+
testee.safeAddDocumentStartJavaScript("script", setOf("*"))
62+
63+
verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(testee, "script", setOf("*"))
64+
}
65+
66+
@Test
67+
fun whenSafeAddDocumentStartJavaScriptWithFeatureDisabledThenDoNotAddScript() = runTest {
68+
whenever(mockWebViewCapabilityChecker.isSupported(DocumentStartJavaScript)).thenReturn(false)
69+
70+
testee.safeAddDocumentStartJavaScript("script", setOf("*"))
71+
72+
verify(mockWebViewCompatWrapper, never()).addDocumentStartJavaScript(testee, "script", setOf("*"))
73+
}
3374
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3021,6 +3021,7 @@ class BrowserTabFragment :
30213021
webView?.let {
30223022
it.isSafeWebViewEnabled = safeWebViewFeature.self().isEnabled()
30233023
it.webViewClient = webViewClient
3024+
webViewClient.configureWebView(it)
30243025
it.webChromeClient = webChromeClient
30253026
it.clearSslPreferences()
30263027

@@ -3205,7 +3206,6 @@ class BrowserTabFragment :
32053206
WebViewCompat.addDocumentStartJavaScript(webView, script, setOf("*"))
32063207

32073208
webView.safeAddWebMessageListener(
3208-
webViewCapabilityChecker,
32093209
"ddgBlobDownloadObj",
32103210
setOf("*"),
32113211
object : WebViewCompat.WebMessageListener {

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import com.duckduckgo.common.utils.AppUrl.ParamKey.QUERY
7070
import com.duckduckgo.common.utils.CurrentTimeProvider
7171
import com.duckduckgo.common.utils.DispatcherProvider
7272
import com.duckduckgo.common.utils.plugins.PluginPoint
73+
import com.duckduckgo.contentscopescripts.api.AddDocumentStartJavaScriptPlugin
7374
import com.duckduckgo.contentscopescripts.api.contentscopeExperiments.ContentScopeExperiments
7475
import com.duckduckgo.cookies.api.CookieManagerProvider
7576
import com.duckduckgo.duckchat.api.DuckChat
@@ -126,6 +127,7 @@ class BrowserWebViewClient @Inject constructor(
126127
private val androidFeaturesHeaderPlugin: AndroidFeaturesHeaderPlugin,
127128
private val duckChat: DuckChat,
128129
private val contentScopeExperiments: ContentScopeExperiments,
130+
private val addDocumentStartJavascriptPlugins: PluginPoint<AddDocumentStartJavaScriptPlugin>,
129131
) : WebViewClient() {
130132

131133
var webViewClientListener: WebViewClientListener? = null
@@ -463,14 +465,41 @@ class BrowserWebViewClient @Inject constructor(
463465
webView.settings.mediaPlaybackRequiresUserGesture = mediaPlayback.doesMediaPlaybackRequireUserGestureForUrl(url)
464466
}
465467

468+
fun configureWebView(webView: DuckDuckGoWebView) {
469+
appCoroutineScope.launch {
470+
val activeExperiments = contentScopeExperiments.getActiveExperiments()
471+
addDocumentStartJavascriptPlugins.getPlugins().forEach { plugin ->
472+
plugin.configureAddDocumentStartJavaScript(activeExperiments) { scriptString, allowedOrigins ->
473+
webView.safeAddDocumentStartJavaScript(scriptString, allowedOrigins)
474+
}
475+
}
476+
}
477+
}
478+
466479
@UiThread
467480
override fun onPageFinished(webView: WebView, url: String?) {
468481
logcat(VERBOSE) { "onPageFinished webViewUrl: ${webView.url} URL: $url progress: ${webView.progress}" }
469482

470483
// See https://app.asana.com/0/0/1206159443951489/f (WebView limitations)
471484
if (webView.progress == 100) {
472-
jsPlugins.getPlugins().forEach {
473-
it.onPageFinished(webView, url, webViewClientListener?.getSite())
485+
appCoroutineScope.launch {
486+
jsPlugins.getPlugins().forEach {
487+
it.onPageFinished(
488+
webView,
489+
url,
490+
webViewClientListener?.getSite(),
491+
)
492+
}
493+
(webView as? DuckDuckGoWebView)?.let { duckDuckGoWebView ->
494+
val activeExperiments = webViewClientListener?.getSite()?.activeContentScopeExperiments ?: listOf()
495+
addDocumentStartJavascriptPlugins.getPlugins().forEach {
496+
it.configureAddDocumentStartJavaScript(
497+
activeExperiments,
498+
) { scriptString, allowedOrigins ->
499+
(webView as? DuckDuckGoWebView)?.safeAddDocumentStartJavaScript(scriptString, allowedOrigins)
500+
}
501+
}
502+
}
474503
}
475504

476505
url?.let {

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,17 @@ import android.webkit.WebViewClient
3333
import androidx.core.view.NestedScrollingChild3
3434
import androidx.core.view.NestedScrollingChildHelper
3535
import androidx.core.view.ViewCompat
36+
import androidx.webkit.ScriptHandler
3637
import androidx.webkit.WebViewCompat
3738
import androidx.webkit.WebViewCompat.WebMessageListener
39+
import com.duckduckgo.anvil.annotations.InjectWith
3840
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker
3941
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability
4042
import com.duckduckgo.app.browser.navigation.safeCopyBackForwardList
43+
import com.duckduckgo.contentscopescripts.impl.WebViewCompatWrapper
44+
import com.duckduckgo.di.scopes.ViewScope
45+
import dagger.android.support.AndroidSupportInjection
46+
import javax.inject.Inject
4147
import logcat.LogPriority.ERROR
4248
import logcat.asLog
4349
import logcat.logcat
@@ -49,6 +55,7 @@ import logcat.logcat
4955
*
5056
* Originally based on https://github.com/takahirom/webview-in-coordinatorlayout for scrolling behaviour
5157
*/
58+
@InjectWith(ViewScope::class)
5259
class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
5360
private var lastClampedTopY: Boolean = true // when created we are always at the top
5461
private var contentAllowsSwipeToRefresh: Boolean = true
@@ -67,6 +74,12 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
6774
private var isDestroyed: Boolean = false
6875
var isSafeWebViewEnabled: Boolean = false
6976

77+
@Inject
78+
lateinit var webViewCompatWrapper: WebViewCompatWrapper
79+
80+
@Inject
81+
lateinit var webViewCapabilityChecker: WebViewCapabilityChecker
82+
7083
constructor(context: Context) : this(context, null)
7184
constructor(
7285
context: Context,
@@ -76,6 +89,7 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
7689
}
7790

7891
override fun onAttachedToWindow() {
92+
AndroidSupportInjection.inject(this)
7993
super.onAttachedToWindow()
8094
helper.onViewAttached(this)
8195
}
@@ -418,7 +432,6 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
418432

419433
@SuppressLint("RequiresFeature", "AddWebMessageListenerUsage")
420434
suspend fun safeAddWebMessageListener(
421-
webViewCapabilityChecker: WebViewCapabilityChecker,
422435
jsObjectName: String,
423436
allowedOriginRules: Set<String>,
424437
listener: WebMessageListener,
@@ -441,7 +454,6 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
441454

442455
@SuppressLint("RequiresFeature", "RemoveWebMessageListenerUsage")
443456
suspend fun safeRemoveWebMessageListener(
444-
webViewCapabilityChecker: WebViewCapabilityChecker,
445457
jsObjectName: String,
446458
): Boolean = runCatching {
447459
if (webViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener) && !isDestroyed) {
@@ -458,6 +470,23 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
458470
false
459471
}
460472

473+
@SuppressLint("RequiresFeature")
474+
suspend fun safeAddDocumentStartJavaScript(
475+
script: String,
476+
allowedOriginRules: Set<String>,
477+
): ScriptHandler? {
478+
return runCatching {
479+
if (webViewCapabilityChecker.isSupported(WebViewCapability.DocumentStartJavaScript) && !isDestroyed) {
480+
webViewCompatWrapper.addDocumentStartJavaScript(this, script, allowedOriginRules)
481+
} else {
482+
null
483+
}
484+
}.getOrElse { e ->
485+
logcat(ERROR) { "Error calling addDocumentStartJavaScript: ${e.asLog()}" }
486+
null
487+
}
488+
}
489+
461490
companion object {
462491

463492
/*
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.AddDocumentStartJavaScriptPlugin
21+
import com.duckduckgo.di.scopes.AppScope
22+
23+
@ContributesPluginPoint(
24+
scope = AppScope::class,
25+
boundType = AddDocumentStartJavaScriptPlugin::class,
26+
)
27+
@Suppress("unused")
28+
interface UnusedJAddDocumentStartJavaScriptPluginPoint

browser-api/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies {
2727
implementation project(path: ':common-utils')
2828
implementation project(':feature-toggles-api')
2929
implementation AndroidX.core.ktx
30+
implementation AndroidX.webkit
3031
implementation KotlinX.coroutines.core
3132

3233
implementation "com.squareup.logcat:logcat:_"

0 commit comments

Comments
 (0)