From 93c76cf30e118b176c193abe7e478b56d39edf9e Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 22 Sep 2025 15:08:06 +0200 Subject: [PATCH 01/11] Add delegate to simplify AddDocumentStartJavaScriptPlugin implementation --- ...ScriptsAddDocumentStartJavaScriptPlugin.kt | 67 ++++---------- .../RealAddDocumentStartScriptDelegate.kt | 87 +++++++++++++++++++ .../api/AddDocumentStartJavaScriptPlugin.kt | 35 ++++++++ 3 files changed, 140 insertions(+), 49 deletions(-) create mode 100644 content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt diff --git a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt index 2177582656b2..c70196d1fb89 100644 --- a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt +++ b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 DuckDuckGo + * Copyright (c) 2025 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,66 +16,35 @@ package com.duckduckgo.contentscopescripts.impl -import android.annotation.SuppressLint -import android.webkit.WebView -import androidx.webkit.ScriptHandler -import com.duckduckgo.app.browser.api.WebViewCapabilityChecker -import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability.DocumentStartJavaScript -import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper -import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.contentscopescripts.api.contentscopeExperiments.ContentScopeExperiments import com.duckduckgo.di.scopes.FragmentScope import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin +import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptScriptStrategy +import com.duckduckgo.js.messaging.api.AddDocumentStartScriptDelegate import com.squareup.anvil.annotations.ContributesMultibinding import dagger.SingleInstanceIn -import kotlinx.coroutines.withContext import javax.inject.Inject @SingleInstanceIn(FragmentScope::class) @ContributesMultibinding(FragmentScope::class) class ContentScopeScriptsAddDocumentStartJavaScriptPlugin @Inject constructor( - private val webViewCompatContentScopeScripts: WebViewCompatContentScopeScripts, - private val dispatcherProvider: DispatcherProvider, - private val webViewCapabilityChecker: WebViewCapabilityChecker, - private val webViewCompatWrapper: WebViewCompatWrapper, - private val contentScopeExperiments: ContentScopeExperiments, -) : AddDocumentStartJavaScriptPlugin { - private var script: ScriptHandler? = null - private var currentScriptString: String? = null - - @SuppressLint("RequiresFeature") - override suspend fun addDocumentStartJavaScript(webView: WebView) { - if (!webViewCompatContentScopeScripts.isEnabled() || - !webViewCapabilityChecker.isSupported( - DocumentStartJavaScript, - ) - ) { - return + webViewCompatContentScopeScripts: WebViewCompatContentScopeScripts, + contentScopeExperiments: ContentScopeExperiments, + scriptInjectorDelegate: AddDocumentStartScriptDelegate, +) : AddDocumentStartJavaScriptPlugin by scriptInjectorDelegate.createPlugin( + object : AddDocumentStartJavaScriptScriptStrategy { + override suspend fun canInject(): Boolean { + return webViewCompatContentScopeScripts.isEnabled() } - val activeExperiments = contentScopeExperiments.getActiveExperiments() - val scriptString = webViewCompatContentScopeScripts.getScript(activeExperiments) - if (scriptString == currentScriptString) { - return - } - script?.let { - withContext(dispatcherProvider.main()) { - it.remove() - } - script = null + override suspend fun getScriptString(): String { + val activeExperiments = contentScopeExperiments.getActiveExperiments() + return webViewCompatContentScopeScripts.getScript(activeExperiments) } - webViewCompatWrapper - .addDocumentStartJavaScript( - webView, - scriptString, - setOf("*"), - )?.let { - script = it - currentScriptString = scriptString - } - } + override val allowedOriginRules: Set = setOf("*") - override val context: String - get() = "contentScopeScripts" -} + override val context: String + get() = "contentScopeScripts" + }, +) diff --git a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt new file mode 100644 index 000000000000..410c14dbf160 --- /dev/null +++ b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.js.messaging.impl + +import android.annotation.SuppressLint +import android.webkit.WebView +import androidx.webkit.ScriptHandler +import com.duckduckgo.app.browser.api.WebViewCapabilityChecker +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin +import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptScriptStrategy +import com.duckduckgo.js.messaging.api.AddDocumentStartScriptDelegate +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@ContributesBinding(FragmentScope::class) +/** + * Delegate class that implements AddDocumentStartJavaScriptPlugin and handles + * the common script injection logic + */ +class RealAddDocumentStartScriptDelegate @Inject constructor( + private val webViewCapabilityChecker: WebViewCapabilityChecker, + @AppCoroutineScope private val coroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, + private val webViewCompatWrapper: WebViewCompatWrapper, +) : AddDocumentStartScriptDelegate { + + private var script: ScriptHandler? = null + private var currentScriptString: String? = null + + override fun createPlugin(strategy: AddDocumentStartJavaScriptScriptStrategy): AddDocumentStartJavaScriptPlugin { + return object : AddDocumentStartJavaScriptPlugin { + @SuppressLint("RequiresFeature") + override suspend fun addDocumentStartJavaScript(webView: WebView) { + if (!strategy.canInject() || !webViewCapabilityChecker.isSupported( + WebViewCapabilityChecker.WebViewCapability.DocumentStartJavaScript, + ) + ) { + return + } + + val scriptString = strategy.getScriptString() + if (scriptString == currentScriptString) { + return + } + script?.let { + withContext(dispatcherProvider.main()) { + it.remove() + } + script = null + } + + webViewCompatWrapper.addDocumentStartJavaScript( + webView, + scriptString, + strategy.allowedOriginRules, + )?.let { + script = it + currentScriptString = scriptString + } + } + + override val context: String + get() = strategy.context + } + } +} diff --git a/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScriptPlugin.kt b/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScriptPlugin.kt index 811a0197425d..66197be3b498 100644 --- a/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScriptPlugin.kt +++ b/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScriptPlugin.kt @@ -28,3 +28,38 @@ interface AddDocumentStartJavaScriptPlugin { val context: String } + +/** + * Strategy interface for script injection logic. + * Allows different implementations to provide their own injection behavior. + */ +interface AddDocumentStartJavaScriptScriptStrategy { + /** + * Determines whether script injection should proceed (i.e. by checking feature flags). + * @return true if injection is allowed, false otherwise + */ + suspend fun canInject(): Boolean + + /** + * Provides the script string to be injected. + * @return the JavaScript code to inject + */ + suspend fun getScriptString(): String + + /** + * Defines the allowed origin rules for script injection. + * @return set of allowed origin patterns + */ + val allowedOriginRules: Set + + val context: String +} + +interface AddDocumentStartScriptDelegate { + /** + * Creates an AddDocumentStartJavaScriptPlugin implementation with the given [AddDocumentStartJavaScriptScriptStrategy]. + * @param strategy the strategy to use for determining injection behavior + * @return [AddDocumentStartJavaScriptPlugin] implementation + */ + fun createPlugin(strategy: AddDocumentStartJavaScriptScriptStrategy): AddDocumentStartJavaScriptPlugin +} From 5d97f6671982b98d0969e2dfe1aa8772c160fcaa Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 22 Sep 2025 17:32:47 +0200 Subject: [PATCH 02/11] Move status to plugin, not delegate --- .../messaging/impl/RealAddDocumentStartScriptDelegate.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt index 410c14dbf160..5958ec6f32db 100644 --- a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt +++ b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt @@ -45,11 +45,12 @@ class RealAddDocumentStartScriptDelegate @Inject constructor( private val webViewCompatWrapper: WebViewCompatWrapper, ) : AddDocumentStartScriptDelegate { - private var script: ScriptHandler? = null - private var currentScriptString: String? = null - override fun createPlugin(strategy: AddDocumentStartJavaScriptScriptStrategy): AddDocumentStartJavaScriptPlugin { return object : AddDocumentStartJavaScriptPlugin { + + private var script: ScriptHandler? = null + private var currentScriptString: String? = null + @SuppressLint("RequiresFeature") override suspend fun addDocumentStartJavaScript(webView: WebView) { if (!strategy.canInject() || !webViewCapabilityChecker.isSupported( From 73f154da6b495b724757b8794828898253cd7a5f Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Tue, 23 Sep 2025 12:55:19 +0200 Subject: [PATCH 03/11] Fix delegate scoping isse and move it to messaging module --- js-messaging/js-messaging-impl/build.gradle | 1 + .../js/messaging/impl/RealAddDocumentStartScriptDelegate.kt | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename {content-scope-scripts/content-scope-scripts-impl => js-messaging/js-messaging-impl}/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt (97%) diff --git a/js-messaging/js-messaging-impl/build.gradle b/js-messaging/js-messaging-impl/build.gradle index 26b4a1b31d2b..f187bf4d0397 100644 --- a/js-messaging/js-messaging-impl/build.gradle +++ b/js-messaging/js-messaging-impl/build.gradle @@ -35,6 +35,7 @@ dependencies { implementation KotlinX.coroutines.core implementation Google.dagger implementation Square.retrofit2.converter.moshi + implementation AndroidX.webkit // Testing dependencies testImplementation "org.mockito.kotlin:mockito-kotlin:_" diff --git a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt b/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt similarity index 97% rename from content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt rename to js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt index 5958ec6f32db..c24849190b46 100644 --- a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt +++ b/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt @@ -23,7 +23,7 @@ import com.duckduckgo.app.browser.api.WebViewCapabilityChecker import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptScriptStrategy import com.duckduckgo.js.messaging.api.AddDocumentStartScriptDelegate @@ -33,7 +33,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -@ContributesBinding(FragmentScope::class) +@ContributesBinding(AppScope::class) /** * Delegate class that implements AddDocumentStartJavaScriptPlugin and handles * the common script injection logic From 9fa97b7b77e5b35b035144decf7667758e076057 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Tue, 23 Sep 2025 17:13:16 +0200 Subject: [PATCH 04/11] Add plugins at the browser level --- .../app/browser/BrowserWebViewClient.kt | 1 + ...ScriptsAddDocumentStartJavaScriptPlugin.kt | 33 +++++++++++++++++++ .../AddDocumentStartJavaScriptPluginPoint.kt | 2 +- browser-api/build.gradle | 1 + .../api/AddDocumentStartJavaScriptPlugin.kt | 23 +++++++++++++ ...ScopeScriptsAddDocumentStartJavaScript.kt} | 8 ++--- ...eScriptsAddDocumentStartJavaScriptTest.kt} | 23 +++++-------- ...lugin.kt => AddDocumentStartJavaScript.kt} | 11 ++++--- .../RealAddDocumentStartScriptDelegate.kt | 6 ++-- 9 files changed, 81 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt create mode 100644 browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptPlugin.kt rename content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/{ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt => ContentScopeScriptsAddDocumentStartJavaScript.kt} (87%) rename content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/{ContentScopeScriptsAddDocumentStartJavaScriptPluginTest.kt => ContentScopeScriptsAddDocumentStartJavaScriptTest.kt} (82%) rename js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/{AddDocumentStartJavaScriptPlugin.kt => AddDocumentStartJavaScript.kt} (90%) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index fdbe66d2274a..f2b24b9997cf 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -65,6 +65,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.InternalTestUserChecker +import com.duckduckgo.browser.api.AddDocumentStartJavaScriptPlugin import com.duckduckgo.browser.api.JsInjectorPlugin import com.duckduckgo.common.utils.AppUrl.ParamKey.QUERY import com.duckduckgo.common.utils.CurrentTimeProvider diff --git a/app/src/main/java/com/duckduckgo/app/browser/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt b/app/src/main/java/com/duckduckgo/app/browser/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt new file mode 100644 index 000000000000..b2e246777b3c --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser + +import com.duckduckgo.browser.api.AddDocumentStartJavaScriptPlugin +import com.duckduckgo.contentscopescripts.impl.ContentScopeScriptsAddDocumentStartJavaScript +import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject + +@ContributesMultibinding(FragmentScope::class) +class ContentScopeScriptsAddDocumentStartJavaScriptPlugin @Inject constructor( + private val contentScopeScriptsAddDocumentStartJavaScript: ContentScopeScriptsAddDocumentStartJavaScript, +) : AddDocumentStartJavaScriptPlugin { + override fun addDocumentStartJavaScript(): AddDocumentStartJavaScript { + return contentScopeScriptsAddDocumentStartJavaScript + } +} diff --git a/app/src/main/java/com/duckduckgo/app/plugins/AddDocumentStartJavaScriptPluginPoint.kt b/app/src/main/java/com/duckduckgo/app/plugins/AddDocumentStartJavaScriptPluginPoint.kt index c3ee19845bed..6b42ba7517b1 100644 --- a/app/src/main/java/com/duckduckgo/app/plugins/AddDocumentStartJavaScriptPluginPoint.kt +++ b/app/src/main/java/com/duckduckgo/app/plugins/AddDocumentStartJavaScriptPluginPoint.kt @@ -17,8 +17,8 @@ package com.duckduckgo.app.plugins import com.duckduckgo.anvil.annotations.ContributesPluginPoint +import com.duckduckgo.browser.api.AddDocumentStartJavaScriptPlugin import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin @ContributesPluginPoint( scope = AppScope::class, diff --git a/browser-api/build.gradle b/browser-api/build.gradle index 5e07d42d6f83..8260f2325bb5 100644 --- a/browser-api/build.gradle +++ b/browser-api/build.gradle @@ -26,6 +26,7 @@ dependencies { implementation project(path: ':design-system') implementation project(path: ':common-utils') implementation project(':feature-toggles-api') + implementation project(':js-messaging-api') implementation AndroidX.core.ktx implementation AndroidX.webkit implementation KotlinX.coroutines.core diff --git a/browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptPlugin.kt b/browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptPlugin.kt new file mode 100644 index 000000000000..f8f4721299d7 --- /dev/null +++ b/browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptPlugin.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.browser.api + +import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript + +interface AddDocumentStartJavaScriptPlugin { + fun addDocumentStartJavaScript(): AddDocumentStartJavaScript +} diff --git a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScript.kt similarity index 87% rename from content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt rename to content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScript.kt index c70196d1fb89..4c6c1a0a8224 100644 --- a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt +++ b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScript.kt @@ -18,20 +18,18 @@ package com.duckduckgo.contentscopescripts.impl import com.duckduckgo.contentscopescripts.api.contentscopeExperiments.ContentScopeExperiments import com.duckduckgo.di.scopes.FragmentScope -import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin +import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptScriptStrategy import com.duckduckgo.js.messaging.api.AddDocumentStartScriptDelegate -import com.squareup.anvil.annotations.ContributesMultibinding import dagger.SingleInstanceIn import javax.inject.Inject @SingleInstanceIn(FragmentScope::class) -@ContributesMultibinding(FragmentScope::class) -class ContentScopeScriptsAddDocumentStartJavaScriptPlugin @Inject constructor( +class ContentScopeScriptsAddDocumentStartJavaScript @Inject constructor( webViewCompatContentScopeScripts: WebViewCompatContentScopeScripts, contentScopeExperiments: ContentScopeExperiments, scriptInjectorDelegate: AddDocumentStartScriptDelegate, -) : AddDocumentStartJavaScriptPlugin by scriptInjectorDelegate.createPlugin( +) : AddDocumentStartJavaScript by scriptInjectorDelegate.createPlugin( object : AddDocumentStartJavaScriptScriptStrategy { override suspend fun canInject(): Boolean { return webViewCompatContentScopeScripts.isEnabled() diff --git a/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptPluginTest.kt b/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptTest.kt similarity index 82% rename from content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptPluginTest.kt rename to content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptTest.kt index 48b6d8646555..fd0da06b5ac1 100644 --- a/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptPluginTest.kt +++ b/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptTest.kt @@ -15,7 +15,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -class ContentScopeScriptsAddDocumentStartJavaScriptPluginTest { +class ContentScopeScriptsAddDocumentStartJavaScriptTest { @get:Rule var coroutineRule = CoroutineTestRule() @@ -25,21 +25,16 @@ class ContentScopeScriptsAddDocumentStartJavaScriptPluginTest { private val mockWebView: WebView = mock() private val mockActiveContentScopeExperiments: ContentScopeExperiments = mock() - private lateinit var testee: ContentScopeScriptsAddDocumentStartJavaScriptPlugin + private lateinit var testee: ContentScopeScriptsAddDocumentStartJavaScript @Before - fun setUp() = - runTest { - whenever(mockActiveContentScopeExperiments.getActiveExperiments()).thenReturn(listOf()) - testee = - ContentScopeScriptsAddDocumentStartJavaScriptPlugin( - mockWebViewCompatContentScopeScripts, - coroutineRule.testDispatcherProvider, - mockWebViewCapabilityChecker, - mockWebViewCompatWrapper, - mockActiveContentScopeExperiments, - ) - } + fun setUp() = runTest { + whenever(mockActiveContentScopeExperiments.getActiveExperiments()).thenReturn(listOf()) + testee = ContentScopeScriptsAddDocumentStartJavaScript( + mockWebViewCompatContentScopeScripts, + mockActiveContentScopeExperiments, + ) + } @Test fun whenFeatureIsEnabledAndCapabilitySupportedThenCallScriptInjectionWithCorrectParams() = diff --git a/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScriptPlugin.kt b/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScript.kt similarity index 90% rename from js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScriptPlugin.kt rename to js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScript.kt index 66197be3b498..9c9c272985be 100644 --- a/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScriptPlugin.kt +++ b/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScript.kt @@ -23,8 +23,11 @@ import android.webkit.WebView * * Allows plugins to inject JavaScript that will be executed before any other scripts on the page. * Useful for privacy protections and that need to run as early as possible and/or on iframes. */ -interface AddDocumentStartJavaScriptPlugin { - suspend fun addDocumentStartJavaScript(webView: WebView) +interface AddDocumentStartJavaScript { + + suspend fun addDocumentStartJavaScript( + webView: WebView, + ) val context: String } @@ -59,7 +62,7 @@ interface AddDocumentStartScriptDelegate { /** * Creates an AddDocumentStartJavaScriptPlugin implementation with the given [AddDocumentStartJavaScriptScriptStrategy]. * @param strategy the strategy to use for determining injection behavior - * @return [AddDocumentStartJavaScriptPlugin] implementation + * @return [AddDocumentStartJavaScript] implementation */ - fun createPlugin(strategy: AddDocumentStartJavaScriptScriptStrategy): AddDocumentStartJavaScriptPlugin + fun createPlugin(strategy: AddDocumentStartJavaScriptScriptStrategy): AddDocumentStartJavaScript } diff --git a/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt b/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt index c24849190b46..8a7b5145aeb8 100644 --- a/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt +++ b/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt @@ -24,7 +24,7 @@ import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin +import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptScriptStrategy import com.duckduckgo.js.messaging.api.AddDocumentStartScriptDelegate import com.squareup.anvil.annotations.ContributesBinding @@ -45,8 +45,8 @@ class RealAddDocumentStartScriptDelegate @Inject constructor( private val webViewCompatWrapper: WebViewCompatWrapper, ) : AddDocumentStartScriptDelegate { - override fun createPlugin(strategy: AddDocumentStartJavaScriptScriptStrategy): AddDocumentStartJavaScriptPlugin { - return object : AddDocumentStartJavaScriptPlugin { + override fun createPlugin(strategy: AddDocumentStartJavaScriptScriptStrategy): AddDocumentStartJavaScript { + return object : AddDocumentStartJavaScript { private var script: ScriptHandler? = null private var currentScriptString: String? = null From 9f123a728f18ab02018b72048a05b8be2fb68f37 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Wed, 24 Sep 2025 17:18:29 +0200 Subject: [PATCH 05/11] Fix tests --- .../app/browser/BrowserWebViewClientTest.kt | 1 + ...peScriptsAddDocumentStartJavaScriptTest.kt | 49 ++----- .../RealAddDocumentStartScriptDelegateTest.kt | 120 ++++++++++++++++++ 3 files changed, 134 insertions(+), 36 deletions(-) create mode 100644 js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt index 37a4a463bfb2..38b1af771594 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt @@ -67,6 +67,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.InternalTestUserChecker +import com.duckduckgo.browser.api.AddDocumentStartJavaScriptPlugin import com.duckduckgo.browser.api.JsInjectorPlugin import com.duckduckgo.browser.api.WebViewVersionProvider import com.duckduckgo.common.test.CoroutineTestRule diff --git a/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptTest.kt b/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptTest.kt index fd0da06b5ac1..1ae63c08592d 100644 --- a/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptTest.kt +++ b/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptTest.kt @@ -1,17 +1,16 @@ package com.duckduckgo.contentscopescripts.impl import android.webkit.WebView -import com.duckduckgo.app.browser.api.WebViewCapabilityChecker -import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.contentscopescripts.api.contentscopeExperiments.ContentScopeExperiments +import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript +import com.duckduckgo.js.messaging.api.AddDocumentStartScriptDelegate import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.mock -import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -20,55 +19,33 @@ class ContentScopeScriptsAddDocumentStartJavaScriptTest { var coroutineRule = CoroutineTestRule() private val mockWebViewCompatContentScopeScripts: WebViewCompatContentScopeScripts = mock() - private val mockWebViewCapabilityChecker: WebViewCapabilityChecker = mock() - private val mockWebViewCompatWrapper: WebViewCompatWrapper = mock() private val mockWebView: WebView = mock() private val mockActiveContentScopeExperiments: ContentScopeExperiments = mock() + private val mockAddDocumentStartScriptDelegate: AddDocumentStartScriptDelegate = mock() + private val mockAddDocumentStartJavaScript: AddDocumentStartJavaScript = mock() private lateinit var testee: ContentScopeScriptsAddDocumentStartJavaScript @Before fun setUp() = runTest { whenever(mockActiveContentScopeExperiments.getActiveExperiments()).thenReturn(listOf()) + whenever(mockAddDocumentStartScriptDelegate.createPlugin(any())).thenReturn(mockAddDocumentStartJavaScript) testee = ContentScopeScriptsAddDocumentStartJavaScript( mockWebViewCompatContentScopeScripts, mockActiveContentScopeExperiments, + mockAddDocumentStartScriptDelegate, ) } @Test - fun whenFeatureIsEnabledAndCapabilitySupportedThenCallScriptInjectionWithCorrectParams() = - runTest { - whenever(mockWebViewCompatContentScopeScripts.isEnabled()).thenReturn(true) - whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) - whenever(mockWebViewCompatContentScopeScripts.getScript(any())).thenReturn("script") + fun whenAddDocumentStartJavaScriptCalledThenDelegateToCreatedPlugin() = runTest { + testee.addDocumentStartJavaScript(mockWebView) - testee.addDocumentStartJavaScript(mockWebView) - - verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(mockWebView, "script", setOf("*")) - } - - @Test - fun whenFeatureIsDisabledAndCapabilitySupportedThenDoNotCallScriptInjection() = - runTest { - whenever(mockWebViewCompatContentScopeScripts.isEnabled()).thenReturn(false) - whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) - whenever(mockWebViewCompatContentScopeScripts.getScript(any())).thenReturn("script") - - testee.addDocumentStartJavaScript(mockWebView) - - verify(mockWebViewCompatWrapper, never()).addDocumentStartJavaScript(any(), any(), any()) - } + verify(mockAddDocumentStartJavaScript).addDocumentStartJavaScript(mockWebView) + } @Test - fun whenFeatureIsEnabledAndCapabilityNotSupportedThenDoNotCallScriptInjection() = - runTest { - whenever(mockWebViewCompatContentScopeScripts.isEnabled()).thenReturn(true) - whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(false) - whenever(mockWebViewCompatContentScopeScripts.getScript(any())).thenReturn("script") - - testee.addDocumentStartJavaScript(mockWebView) - - verify(mockWebViewCompatWrapper, never()).addDocumentStartJavaScript(any(), any(), any()) - } + fun whenConstructedThenCreatePluginWithCorrectStrategy() = runTest { + verify(mockAddDocumentStartScriptDelegate).createPlugin(any()) + } } diff --git a/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt b/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt new file mode 100644 index 000000000000..265b27e22dd0 --- /dev/null +++ b/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt @@ -0,0 +1,120 @@ +package com.duckduckgo.js.messaging.impl + +import android.webkit.WebView +import androidx.webkit.ScriptHandler +import com.duckduckgo.app.browser.api.WebViewCapabilityChecker +import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript +import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptScriptStrategy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class RealAddDocumentStartScriptDelegateTest { + + @get:Rule + var coroutineRule = CoroutineTestRule() + + private val mockWebViewCapabilityChecker: WebViewCapabilityChecker = mock() + private val mockWebViewCompatWrapper: WebViewCompatWrapper = mock() + private val mockWebView: WebView = mock() + private val mockScriptHandler: ScriptHandler = mock() + private val mockDispatcherProvider: DispatcherProvider = mock() + + private lateinit var testee: RealAddDocumentStartScriptDelegate + private lateinit var plugin: AddDocumentStartJavaScript + + @Before + fun setUp() = runTest { + whenever(mockDispatcherProvider.main()).thenReturn(coroutineRule.testDispatcher) + testee = RealAddDocumentStartScriptDelegate( + mockWebViewCapabilityChecker, + coroutineRule.testScope, + mockDispatcherProvider, + mockWebViewCompatWrapper, + ) + } + + @Test + fun whenFeatureEnabledAndCapabilitySupportedThenInjectScript() = runTest { + val mockStrategy = createMockStrategy(canInject = true, scriptString = "test script") + plugin = testee.createPlugin(mockStrategy) + whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) + whenever(mockWebViewCompatWrapper.addDocumentStartJavaScript(any(), any(), any())).thenReturn(mockScriptHandler) + + plugin.addDocumentStartJavaScript(mockWebView) + + verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(mockWebView, "test script", setOf("*")) + } + + @Test + fun whenFeatureDisabledThenDoNotInjectScript() = runTest { + val mockStrategy = createMockStrategy(canInject = false, scriptString = "test script") + plugin = testee.createPlugin(mockStrategy) + whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) + + plugin.addDocumentStartJavaScript(mockWebView) + + verify(mockWebViewCompatWrapper, never()).addDocumentStartJavaScript(any(), any(), any()) + } + + @Test + fun whenCapabilityNotSupportedThenDoNotInjectScript() = runTest { + val mockStrategy = createMockStrategy(canInject = true, scriptString = "test script") + plugin = testee.createPlugin(mockStrategy) + whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(false) + + plugin.addDocumentStartJavaScript(mockWebView) + + verify(mockWebViewCompatWrapper, never()).addDocumentStartJavaScript(any(), any(), any()) + } + + @Test + fun whenScriptStringSameAsCurrentThenDoNotReinject() = runTest { + val mockStrategy = createMockStrategy(canInject = true, scriptString = "test script") + plugin = testee.createPlugin(mockStrategy) + whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) + whenever(mockWebViewCompatWrapper.addDocumentStartJavaScript(any(), any(), any())).thenReturn(mockScriptHandler) + + plugin.addDocumentStartJavaScript(mockWebView) + plugin.addDocumentStartJavaScript(mockWebView) + + verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(mockWebView, "test script", setOf("*")) + } + + @Test + fun whenScriptStringDifferentThenRemoveOldAndInjectNew() = runTest { + val mockStrategy: AddDocumentStartJavaScriptScriptStrategy = mock() + whenever(mockStrategy.canInject()).thenReturn(true) + whenever(mockStrategy.getScriptString()).thenReturn("script 1") + whenever(mockStrategy.allowedOriginRules).thenReturn(setOf("*")) + plugin = testee.createPlugin(mockStrategy) + whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) + whenever(mockWebViewCompatWrapper.addDocumentStartJavaScript(any(), any(), any())).thenReturn(mockScriptHandler) + + plugin.addDocumentStartJavaScript(mockWebView) + + whenever(mockStrategy.getScriptString()).thenReturn("script 2") + + plugin.addDocumentStartJavaScript(mockWebView) + + verify(mockScriptHandler).remove() + verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(mockWebView, "script 2", setOf("*")) + } + + private fun createMockStrategy(canInject: Boolean, scriptString: String): AddDocumentStartJavaScriptScriptStrategy { + return object : AddDocumentStartJavaScriptScriptStrategy { + override suspend fun canInject(): Boolean = canInject + override suspend fun getScriptString(): String = scriptString + override val allowedOriginRules: Set = setOf("*") + } + } +} From 85ec85cfb810db01f0569955ea94b2a0d60ec69d Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Wed, 1 Oct 2025 18:13:37 +0200 Subject: [PATCH 06/11] Fix issues after rebasing --- .../app/browser/BrowserTabViewModelTest.kt | 54 ++++---- .../app/browser/BrowserWebViewClientTest.kt | 1 - .../app/browser/BrowserTabViewModel.kt | 7 +- .../app/browser/BrowserWebViewClient.kt | 1 - ...ddDocumentStartJavaScriptBrowserPlugin.kt} | 10 +- .../AddDocumentStartJavaScriptPluginPoint.kt | 4 +- ...ddDocumentStartJavaScriptBrowserPlugin.kt} | 2 +- ...tScopeScriptsAddDocumentStartJavaScript.kt | 4 +- ...peScriptsAddDocumentStartJavaScriptTest.kt | 36 ++--- .../api/AddDocumentStartJavaScript.kt | 5 +- .../RealAddDocumentStartScriptDelegate.kt | 27 ++-- .../RealAddDocumentStartScriptDelegateTest.kt | 126 ++++++++++-------- 12 files changed, 143 insertions(+), 134 deletions(-) rename app/src/main/java/com/duckduckgo/app/browser/{ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt => ContentScopeScriptsAddDocumentStartJavaScriptBrowserPlugin.kt} (81%) rename browser-api/src/main/java/com/duckduckgo/browser/api/{AddDocumentStartJavaScriptPlugin.kt => AddDocumentStartJavaScriptBrowserPlugin.kt} (93%) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index cf77eee3da7c..6b3ba46ae093 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -208,6 +208,7 @@ import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonito import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor import com.duckduckgo.brokensite.api.BrokenSitePrompt import com.duckduckgo.brokensite.api.RefreshPattern +import com.duckduckgo.browser.api.AddDocumentStartJavaScriptBrowserPlugin import com.duckduckgo.browser.api.UserBrowserProperties import com.duckduckgo.browser.api.autocomplete.AutoComplete import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggestion.AutoCompleteDefaultSuggestion @@ -255,7 +256,7 @@ import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.feature.toggles.api.Toggle.State import com.duckduckgo.history.api.HistoryEntry.VisitedPage import com.duckduckgo.history.api.NavigationHistory -import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin +import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript import com.duckduckgo.js.messaging.api.JsCallbackData import com.duckduckgo.js.messaging.api.PostMessageWrapperPlugin import com.duckduckgo.js.messaging.api.SubscriptionEventData @@ -6987,8 +6988,8 @@ class BrowserTabViewModelTest { testee.pageFinished(mockWebView, webViewNavState, url) - assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.countInitted) - assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.otherPlugin.countInitted) + assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.addDocumentStartJavaScript().countInitted) + assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.otherPlugin.addDocumentStartJavaScript().countInitted) } @Test @@ -7000,8 +7001,8 @@ class BrowserTabViewModelTest { testee.pageFinished(mockWebView, webViewNavState, url) - assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.countInitted) - assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.otherPlugin.countInitted) + assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.addDocumentStartJavaScript().countInitted) + assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.otherPlugin.addDocumentStartJavaScript().countInitted) } @Test @@ -7011,8 +7012,8 @@ class BrowserTabViewModelTest { testee.privacyProtectionsUpdated(mockWebView) - assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.countInitted) - assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.otherPlugin.countInitted) + assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.addDocumentStartJavaScript().countInitted) + assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.otherPlugin.addDocumentStartJavaScript().countInitted) } @Test @@ -7022,8 +7023,8 @@ class BrowserTabViewModelTest { testee.privacyProtectionsUpdated(mockWebView) - assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.countInitted) - assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.otherPlugin.countInitted) + assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.addDocumentStartJavaScript().countInitted) + assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.otherPlugin.addDocumentStartJavaScript().countInitted) } @Test @@ -7512,11 +7513,11 @@ class BrowserTabViewModelTest { runTest { val mockCallback = mock() val webView = DuckDuckGoWebView(context) - assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.countInitted) + assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.addDocumentStartJavaScript().countInitted) testee.configureWebView(webView, mockCallback) - assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.countInitted) + assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.cssPlugin.addDocumentStartJavaScript().countInitted) } @UiThreadTest @@ -7833,9 +7834,13 @@ class BrowserTabViewModelTest { override fun getCustomHeaders(url: String): Map = headers } - class FakeAddDocumentStartJavaScriptPlugin( - override val context: String, - ) : AddDocumentStartJavaScriptPlugin { + class FakeAddDocumentStartJavaScriptBrowserPlugin(val context: String) : AddDocumentStartJavaScriptBrowserPlugin { + private val addDocumentStartJavaScript = FakeAddDocumentStartJavaScript(context) + + override fun addDocumentStartJavaScript(): FakeAddDocumentStartJavaScript = addDocumentStartJavaScript + } + + class FakeAddDocumentStartJavaScript(override val context: String) : AddDocumentStartJavaScript { var countInitted = 0 private set @@ -7844,13 +7849,19 @@ class BrowserTabViewModelTest { } } - class FakeAddDocumentStartJavaScriptPluginPoint : PluginPoint { - val cssPlugin = FakeAddDocumentStartJavaScriptPlugin("contentScopeScripts") - val otherPlugin = FakeAddDocumentStartJavaScriptPlugin("test") + class FakeAddDocumentStartJavaScriptPluginPoint : PluginPoint { + val cssPlugin = FakeAddDocumentStartJavaScriptBrowserPlugin("contentScopeScripts") + val otherPlugin = FakeAddDocumentStartJavaScriptBrowserPlugin("test") override fun getPlugins() = listOf(cssPlugin, otherPlugin) } + class FakePostMessageWrapperPluginPoint : PluginPoint { + val plugin = FakePostMessageWrapperPlugin() + + override fun getPlugins(): Collection = listOf(plugin) + } + class FakeWebMessagingPlugin : WebMessagingPlugin { var registered = false private set @@ -7891,15 +7902,8 @@ class BrowserTabViewModelTest { webView: WebView, ) { postMessageCalled = true - } - + } override val context: String get() = "contentScopeScripts" } - - class FakePostMessageWrapperPluginPoint : PluginPoint { - val plugin = FakePostMessageWrapperPlugin() - - override fun getPlugins(): Collection = listOf(plugin) - } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt index 38b1af771594..37a4a463bfb2 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt @@ -67,7 +67,6 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.InternalTestUserChecker -import com.duckduckgo.browser.api.AddDocumentStartJavaScriptPlugin import com.duckduckgo.browser.api.JsInjectorPlugin import com.duckduckgo.browser.api.WebViewVersionProvider import com.duckduckgo.common.test.CoroutineTestRule diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 3e7601239751..e7a38df800ec 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -274,6 +274,7 @@ import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonito import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor import com.duckduckgo.brokensite.api.BrokenSitePrompt import com.duckduckgo.brokensite.api.RefreshPattern +import com.duckduckgo.browser.api.AddDocumentStartJavaScriptBrowserPlugin import com.duckduckgo.browser.api.UserBrowserProperties import com.duckduckgo.browser.api.autocomplete.AutoComplete import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteResult @@ -319,7 +320,6 @@ import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.history.api.NavigationHistory -import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin import com.duckduckgo.js.messaging.api.JsCallbackData import com.duckduckgo.js.messaging.api.PostMessageWrapperPlugin import com.duckduckgo.js.messaging.api.SubscriptionEventData @@ -489,7 +489,7 @@ class BrowserTabViewModel @Inject constructor( private val externalIntentProcessingState: ExternalIntentProcessingState, private val vpnMenuStateProvider: VpnMenuStateProvider, private val webViewCompatWrapper: WebViewCompatWrapper, - private val addDocumentStartJavascriptPlugins: PluginPoint, + private val addDocumentStartJavascriptPlugins: PluginPoint, private val webMessagingPlugins: PluginPoint, private val postMessageWrapperPlugins: PluginPoint, ) : ViewModel(), @@ -4247,7 +4247,7 @@ class BrowserTabViewModel @Inject constructor( } private suspend fun addDocumentStartJavaScript(webView: WebView) { - addDocumentStartJavascriptPlugins.getPlugins().forEach { + addDocumentStartJavascriptPlugins.getPlugins().addDocumentStartJavaScript().forEach { it.addDocumentStartJavaScript( webView, ) @@ -4258,6 +4258,7 @@ class BrowserTabViewModel @Inject constructor( if (withContext(dispatchers.io()) { !androidBrowserConfig.updateScriptOnPageFinished().isEnabled() }) { addDocumentStartJavascriptPlugins .getPlugins() + .addDocumentStartJavaScript() .filter { plugin -> (plugin.context == "contentScopeScripts") }.forEach { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index f2b24b9997cf..fdbe66d2274a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -65,7 +65,6 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.InternalTestUserChecker -import com.duckduckgo.browser.api.AddDocumentStartJavaScriptPlugin import com.duckduckgo.browser.api.JsInjectorPlugin import com.duckduckgo.common.utils.AppUrl.ParamKey.QUERY import com.duckduckgo.common.utils.CurrentTimeProvider diff --git a/app/src/main/java/com/duckduckgo/app/browser/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt b/app/src/main/java/com/duckduckgo/app/browser/ContentScopeScriptsAddDocumentStartJavaScriptBrowserPlugin.kt similarity index 81% rename from app/src/main/java/com/duckduckgo/app/browser/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt rename to app/src/main/java/com/duckduckgo/app/browser/ContentScopeScriptsAddDocumentStartJavaScriptBrowserPlugin.kt index b2e246777b3c..7c9b52935f22 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/ContentScopeScriptsAddDocumentStartJavaScriptPlugin.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/ContentScopeScriptsAddDocumentStartJavaScriptBrowserPlugin.kt @@ -16,7 +16,7 @@ package com.duckduckgo.app.browser -import com.duckduckgo.browser.api.AddDocumentStartJavaScriptPlugin +import com.duckduckgo.browser.api.AddDocumentStartJavaScriptBrowserPlugin import com.duckduckgo.contentscopescripts.impl.ContentScopeScriptsAddDocumentStartJavaScript import com.duckduckgo.di.scopes.FragmentScope import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript @@ -24,10 +24,8 @@ import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject @ContributesMultibinding(FragmentScope::class) -class ContentScopeScriptsAddDocumentStartJavaScriptPlugin @Inject constructor( +class ContentScopeScriptsAddDocumentStartJavaScriptBrowserPlugin @Inject constructor( private val contentScopeScriptsAddDocumentStartJavaScript: ContentScopeScriptsAddDocumentStartJavaScript, -) : AddDocumentStartJavaScriptPlugin { - override fun addDocumentStartJavaScript(): AddDocumentStartJavaScript { - return contentScopeScriptsAddDocumentStartJavaScript - } +) : AddDocumentStartJavaScriptBrowserPlugin { + override fun addDocumentStartJavaScript(): AddDocumentStartJavaScript = contentScopeScriptsAddDocumentStartJavaScript } diff --git a/app/src/main/java/com/duckduckgo/app/plugins/AddDocumentStartJavaScriptPluginPoint.kt b/app/src/main/java/com/duckduckgo/app/plugins/AddDocumentStartJavaScriptPluginPoint.kt index 6b42ba7517b1..34358e919c6f 100644 --- a/app/src/main/java/com/duckduckgo/app/plugins/AddDocumentStartJavaScriptPluginPoint.kt +++ b/app/src/main/java/com/duckduckgo/app/plugins/AddDocumentStartJavaScriptPluginPoint.kt @@ -17,12 +17,12 @@ package com.duckduckgo.app.plugins import com.duckduckgo.anvil.annotations.ContributesPluginPoint -import com.duckduckgo.browser.api.AddDocumentStartJavaScriptPlugin +import com.duckduckgo.browser.api.AddDocumentStartJavaScriptBrowserPlugin import com.duckduckgo.di.scopes.AppScope @ContributesPluginPoint( scope = AppScope::class, - boundType = AddDocumentStartJavaScriptPlugin::class, + boundType = AddDocumentStartJavaScriptBrowserPlugin::class, ) @Suppress("unused") interface UnusedJAddDocumentStartJavaScriptPluginPoint diff --git a/browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptPlugin.kt b/browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptBrowserPlugin.kt similarity index 93% rename from browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptPlugin.kt rename to browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptBrowserPlugin.kt index f8f4721299d7..f52b8789f8e8 100644 --- a/browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptPlugin.kt +++ b/browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptBrowserPlugin.kt @@ -18,6 +18,6 @@ package com.duckduckgo.browser.api import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript -interface AddDocumentStartJavaScriptPlugin { +interface AddDocumentStartJavaScriptBrowserPlugin { fun addDocumentStartJavaScript(): AddDocumentStartJavaScript } diff --git a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScript.kt b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScript.kt index 4c6c1a0a8224..b346d1c38a16 100644 --- a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScript.kt +++ b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScript.kt @@ -31,9 +31,7 @@ class ContentScopeScriptsAddDocumentStartJavaScript @Inject constructor( scriptInjectorDelegate: AddDocumentStartScriptDelegate, ) : AddDocumentStartJavaScript by scriptInjectorDelegate.createPlugin( object : AddDocumentStartJavaScriptScriptStrategy { - override suspend fun canInject(): Boolean { - return webViewCompatContentScopeScripts.isEnabled() - } + override suspend fun canInject(): Boolean = webViewCompatContentScopeScripts.isEnabled() override suspend fun getScriptString(): String { val activeExperiments = contentScopeExperiments.getActiveExperiments() diff --git a/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptTest.kt b/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptTest.kt index 1ae63c08592d..1985b7f40389 100644 --- a/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptTest.kt +++ b/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsAddDocumentStartJavaScriptTest.kt @@ -27,25 +27,29 @@ class ContentScopeScriptsAddDocumentStartJavaScriptTest { private lateinit var testee: ContentScopeScriptsAddDocumentStartJavaScript @Before - fun setUp() = runTest { - whenever(mockActiveContentScopeExperiments.getActiveExperiments()).thenReturn(listOf()) - whenever(mockAddDocumentStartScriptDelegate.createPlugin(any())).thenReturn(mockAddDocumentStartJavaScript) - testee = ContentScopeScriptsAddDocumentStartJavaScript( - mockWebViewCompatContentScopeScripts, - mockActiveContentScopeExperiments, - mockAddDocumentStartScriptDelegate, - ) - } + fun setUp() = + runTest { + whenever(mockActiveContentScopeExperiments.getActiveExperiments()).thenReturn(listOf()) + whenever(mockAddDocumentStartScriptDelegate.createPlugin(any())).thenReturn(mockAddDocumentStartJavaScript) + testee = + ContentScopeScriptsAddDocumentStartJavaScript( + mockWebViewCompatContentScopeScripts, + mockActiveContentScopeExperiments, + mockAddDocumentStartScriptDelegate, + ) + } @Test - fun whenAddDocumentStartJavaScriptCalledThenDelegateToCreatedPlugin() = runTest { - testee.addDocumentStartJavaScript(mockWebView) + fun whenAddDocumentStartJavaScriptCalledThenDelegateToCreatedPlugin() = + runTest { + testee.addDocumentStartJavaScript(mockWebView) - verify(mockAddDocumentStartJavaScript).addDocumentStartJavaScript(mockWebView) - } + verify(mockAddDocumentStartJavaScript).addDocumentStartJavaScript(mockWebView) + } @Test - fun whenConstructedThenCreatePluginWithCorrectStrategy() = runTest { - verify(mockAddDocumentStartScriptDelegate).createPlugin(any()) - } + fun whenConstructedThenCreatePluginWithCorrectStrategy() = + runTest { + verify(mockAddDocumentStartScriptDelegate).createPlugin(any()) + } } diff --git a/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScript.kt b/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScript.kt index 9c9c272985be..2a7e040136d8 100644 --- a/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScript.kt +++ b/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScript.kt @@ -24,10 +24,7 @@ import android.webkit.WebView * Useful for privacy protections and that need to run as early as possible and/or on iframes. */ interface AddDocumentStartJavaScript { - - suspend fun addDocumentStartJavaScript( - webView: WebView, - ) + suspend fun addDocumentStartJavaScript(webView: WebView) val context: String } diff --git a/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt b/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt index 8a7b5145aeb8..bd0d67c578d2 100644 --- a/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt +++ b/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt @@ -28,32 +28,30 @@ import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptScriptStrategy import com.duckduckgo.js.messaging.api.AddDocumentStartScriptDelegate import com.squareup.anvil.annotations.ContributesBinding -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import javax.inject.Inject -@ContributesBinding(AppScope::class) /** * Delegate class that implements AddDocumentStartJavaScriptPlugin and handles * the common script injection logic */ +@ContributesBinding(AppScope::class) class RealAddDocumentStartScriptDelegate @Inject constructor( private val webViewCapabilityChecker: WebViewCapabilityChecker, @AppCoroutineScope private val coroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, private val webViewCompatWrapper: WebViewCompatWrapper, ) : AddDocumentStartScriptDelegate { - override fun createPlugin(strategy: AddDocumentStartJavaScriptScriptStrategy): AddDocumentStartJavaScript { return object : AddDocumentStartJavaScript { - private var script: ScriptHandler? = null private var currentScriptString: String? = null @SuppressLint("RequiresFeature") override suspend fun addDocumentStartJavaScript(webView: WebView) { - if (!strategy.canInject() || !webViewCapabilityChecker.isSupported( + if (!strategy.canInject() || + !webViewCapabilityChecker.isSupported( WebViewCapabilityChecker.WebViewCapability.DocumentStartJavaScript, ) ) { @@ -71,14 +69,15 @@ class RealAddDocumentStartScriptDelegate @Inject constructor( script = null } - webViewCompatWrapper.addDocumentStartJavaScript( - webView, - scriptString, - strategy.allowedOriginRules, - )?.let { - script = it - currentScriptString = scriptString - } + webViewCompatWrapper + .addDocumentStartJavaScript( + webView, + scriptString, + strategy.allowedOriginRules, + )?.let { + script = it + currentScriptString = scriptString + } } override val context: String diff --git a/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt b/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt index 265b27e22dd0..441804046908 100644 --- a/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt +++ b/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt @@ -19,7 +19,6 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever class RealAddDocumentStartScriptDelegateTest { - @get:Rule var coroutineRule = CoroutineTestRule() @@ -33,88 +32,99 @@ class RealAddDocumentStartScriptDelegateTest { private lateinit var plugin: AddDocumentStartJavaScript @Before - fun setUp() = runTest { - whenever(mockDispatcherProvider.main()).thenReturn(coroutineRule.testDispatcher) - testee = RealAddDocumentStartScriptDelegate( - mockWebViewCapabilityChecker, - coroutineRule.testScope, - mockDispatcherProvider, - mockWebViewCompatWrapper, - ) - } + fun setUp() = + runTest { + whenever(mockDispatcherProvider.main()).thenReturn(coroutineRule.testDispatcher) + testee = + RealAddDocumentStartScriptDelegate( + mockWebViewCapabilityChecker, + coroutineRule.testScope, + mockDispatcherProvider, + mockWebViewCompatWrapper, + ) + } @Test - fun whenFeatureEnabledAndCapabilitySupportedThenInjectScript() = runTest { - val mockStrategy = createMockStrategy(canInject = true, scriptString = "test script") - plugin = testee.createPlugin(mockStrategy) - whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) - whenever(mockWebViewCompatWrapper.addDocumentStartJavaScript(any(), any(), any())).thenReturn(mockScriptHandler) + fun whenFeatureEnabledAndCapabilitySupportedThenInjectScript() = + runTest { + val mockStrategy = createMockStrategy(canInject = true, scriptString = "test script") + plugin = testee.createPlugin(mockStrategy) + whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) + whenever(mockWebViewCompatWrapper.addDocumentStartJavaScript(any(), any(), any())).thenReturn(mockScriptHandler) - plugin.addDocumentStartJavaScript(mockWebView) + plugin.addDocumentStartJavaScript(mockWebView) - verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(mockWebView, "test script", setOf("*")) - } + verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(mockWebView, "test script", setOf("*")) + } @Test - fun whenFeatureDisabledThenDoNotInjectScript() = runTest { - val mockStrategy = createMockStrategy(canInject = false, scriptString = "test script") - plugin = testee.createPlugin(mockStrategy) - whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) + fun whenFeatureDisabledThenDoNotInjectScript() = + runTest { + val mockStrategy = createMockStrategy(canInject = false, scriptString = "test script") + plugin = testee.createPlugin(mockStrategy) + whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) - plugin.addDocumentStartJavaScript(mockWebView) + plugin.addDocumentStartJavaScript(mockWebView) - verify(mockWebViewCompatWrapper, never()).addDocumentStartJavaScript(any(), any(), any()) - } + verify(mockWebViewCompatWrapper, never()).addDocumentStartJavaScript(any(), any(), any()) + } @Test - fun whenCapabilityNotSupportedThenDoNotInjectScript() = runTest { - val mockStrategy = createMockStrategy(canInject = true, scriptString = "test script") - plugin = testee.createPlugin(mockStrategy) - whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(false) + fun whenCapabilityNotSupportedThenDoNotInjectScript() = + runTest { + val mockStrategy = createMockStrategy(canInject = true, scriptString = "test script") + plugin = testee.createPlugin(mockStrategy) + whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(false) - plugin.addDocumentStartJavaScript(mockWebView) + plugin.addDocumentStartJavaScript(mockWebView) - verify(mockWebViewCompatWrapper, never()).addDocumentStartJavaScript(any(), any(), any()) - } + verify(mockWebViewCompatWrapper, never()).addDocumentStartJavaScript(any(), any(), any()) + } @Test - fun whenScriptStringSameAsCurrentThenDoNotReinject() = runTest { - val mockStrategy = createMockStrategy(canInject = true, scriptString = "test script") - plugin = testee.createPlugin(mockStrategy) - whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) - whenever(mockWebViewCompatWrapper.addDocumentStartJavaScript(any(), any(), any())).thenReturn(mockScriptHandler) + fun whenScriptStringSameAsCurrentThenDoNotReinject() = + runTest { + val mockStrategy = createMockStrategy(canInject = true, scriptString = "test script") + plugin = testee.createPlugin(mockStrategy) + whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) + whenever(mockWebViewCompatWrapper.addDocumentStartJavaScript(any(), any(), any())).thenReturn(mockScriptHandler) - plugin.addDocumentStartJavaScript(mockWebView) - plugin.addDocumentStartJavaScript(mockWebView) + plugin.addDocumentStartJavaScript(mockWebView) + plugin.addDocumentStartJavaScript(mockWebView) - verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(mockWebView, "test script", setOf("*")) - } + verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(mockWebView, "test script", setOf("*")) + } @Test - fun whenScriptStringDifferentThenRemoveOldAndInjectNew() = runTest { - val mockStrategy: AddDocumentStartJavaScriptScriptStrategy = mock() - whenever(mockStrategy.canInject()).thenReturn(true) - whenever(mockStrategy.getScriptString()).thenReturn("script 1") - whenever(mockStrategy.allowedOriginRules).thenReturn(setOf("*")) - plugin = testee.createPlugin(mockStrategy) - whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) - whenever(mockWebViewCompatWrapper.addDocumentStartJavaScript(any(), any(), any())).thenReturn(mockScriptHandler) + fun whenScriptStringDifferentThenRemoveOldAndInjectNew() = + runTest { + val mockStrategy: AddDocumentStartJavaScriptScriptStrategy = mock() + whenever(mockStrategy.canInject()).thenReturn(true) + whenever(mockStrategy.getScriptString()).thenReturn("script 1") + whenever(mockStrategy.allowedOriginRules).thenReturn(setOf("*")) + plugin = testee.createPlugin(mockStrategy) + whenever(mockWebViewCapabilityChecker.isSupported(any())).thenReturn(true) + whenever(mockWebViewCompatWrapper.addDocumentStartJavaScript(any(), any(), any())).thenReturn(mockScriptHandler) - plugin.addDocumentStartJavaScript(mockWebView) + plugin.addDocumentStartJavaScript(mockWebView) - whenever(mockStrategy.getScriptString()).thenReturn("script 2") + whenever(mockStrategy.getScriptString()).thenReturn("script 2") - plugin.addDocumentStartJavaScript(mockWebView) + plugin.addDocumentStartJavaScript(mockWebView) - verify(mockScriptHandler).remove() - verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(mockWebView, "script 2", setOf("*")) - } + verify(mockScriptHandler).remove() + verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(mockWebView, "script 2", setOf("*")) + } - private fun createMockStrategy(canInject: Boolean, scriptString: String): AddDocumentStartJavaScriptScriptStrategy { - return object : AddDocumentStartJavaScriptScriptStrategy { + private fun createMockStrategy( + canInject: Boolean, + scriptString: String, + ): AddDocumentStartJavaScriptScriptStrategy = + object : AddDocumentStartJavaScriptScriptStrategy { override suspend fun canInject(): Boolean = canInject + override suspend fun getScriptString(): String = scriptString + override val allowedOriginRules: Set = setOf("*") } - } } From 399977cc12e0aee8f5f400964df06eacd8704123 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 2 Oct 2025 12:05:20 +0200 Subject: [PATCH 07/11] Fix formatting --- .../java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 6b3ba46ae093..4fd3554a142c 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -7902,7 +7902,7 @@ class BrowserTabViewModelTest { webView: WebView, ) { postMessageCalled = true - } + } override val context: String get() = "contentScopeScripts" } From b7752666eefdc39cb1ff4366e7836d98f372a504 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 2 Oct 2025 12:29:09 +0200 Subject: [PATCH 08/11] Fix issues after rebase --- .../com/duckduckgo/app/browser/BrowserTabViewModel.kt | 8 +++----- .../impl/RealAddDocumentStartScriptDelegateTest.kt | 3 +++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index e7a38df800ec..54eb64147206 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -4247,10 +4247,8 @@ class BrowserTabViewModel @Inject constructor( } private suspend fun addDocumentStartJavaScript(webView: WebView) { - addDocumentStartJavascriptPlugins.getPlugins().addDocumentStartJavaScript().forEach { - it.addDocumentStartJavaScript( - webView, - ) + addDocumentStartJavascriptPlugins.getPlugins().forEach { + it.addDocumentStartJavaScript().addDocumentStartJavaScript(webView) } } @@ -4258,7 +4256,7 @@ class BrowserTabViewModel @Inject constructor( if (withContext(dispatchers.io()) { !androidBrowserConfig.updateScriptOnPageFinished().isEnabled() }) { addDocumentStartJavascriptPlugins .getPlugins() - .addDocumentStartJavaScript() + .map { it.addDocumentStartJavaScript() } .filter { plugin -> (plugin.context == "contentScopeScripts") }.forEach { diff --git a/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt b/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt index 441804046908..d14067077b90 100644 --- a/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt +++ b/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt @@ -126,5 +126,8 @@ class RealAddDocumentStartScriptDelegateTest { override suspend fun getScriptString(): String = scriptString override val allowedOriginRules: Set = setOf("*") + + override val context: String + get() = "test" } } From c40c4837516a2e893399b2da71a784eef0784218 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 2 Oct 2025 15:32:17 +0200 Subject: [PATCH 09/11] Remove unused param --- .../messaging/impl/RealAddDocumentStartScriptDelegate.kt | 3 --- .../impl/RealAddDocumentStartScriptDelegateTest.kt | 7 +------ 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt b/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt index bd0d67c578d2..bffc65e04aba 100644 --- a/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt +++ b/js-messaging/js-messaging-impl/src/main/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegate.kt @@ -20,7 +20,6 @@ import android.annotation.SuppressLint import android.webkit.WebView import androidx.webkit.ScriptHandler import com.duckduckgo.app.browser.api.WebViewCapabilityChecker -import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope @@ -28,7 +27,6 @@ import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptScriptStrategy import com.duckduckgo.js.messaging.api.AddDocumentStartScriptDelegate import com.squareup.anvil.annotations.ContributesBinding -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext import javax.inject.Inject @@ -39,7 +37,6 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class RealAddDocumentStartScriptDelegate @Inject constructor( private val webViewCapabilityChecker: WebViewCapabilityChecker, - @AppCoroutineScope private val coroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, private val webViewCompatWrapper: WebViewCompatWrapper, ) : AddDocumentStartScriptDelegate { diff --git a/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt b/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt index d14067077b90..0c51c0dcc85d 100644 --- a/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt +++ b/js-messaging/js-messaging-impl/src/test/java/com/duckduckgo/js/messaging/impl/RealAddDocumentStartScriptDelegateTest.kt @@ -5,7 +5,6 @@ import androidx.webkit.ScriptHandler import com.duckduckgo.app.browser.api.WebViewCapabilityChecker import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper import com.duckduckgo.common.test.CoroutineTestRule -import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptScriptStrategy import kotlinx.coroutines.test.runTest @@ -26,20 +25,16 @@ class RealAddDocumentStartScriptDelegateTest { private val mockWebViewCompatWrapper: WebViewCompatWrapper = mock() private val mockWebView: WebView = mock() private val mockScriptHandler: ScriptHandler = mock() - private val mockDispatcherProvider: DispatcherProvider = mock() - private lateinit var testee: RealAddDocumentStartScriptDelegate private lateinit var plugin: AddDocumentStartJavaScript @Before fun setUp() = runTest { - whenever(mockDispatcherProvider.main()).thenReturn(coroutineRule.testDispatcher) testee = RealAddDocumentStartScriptDelegate( mockWebViewCapabilityChecker, - coroutineRule.testScope, - mockDispatcherProvider, + coroutineRule.testDispatcherProvider, mockWebViewCompatWrapper, ) } From 19995c797f8956cdc53ece8fa38f73fcb518999e Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 2 Oct 2025 16:04:23 +0200 Subject: [PATCH 10/11] Update documentation --- .../AddDocumentStartJavaScriptBrowserPlugin.kt | 9 +++++++++ .../api/AddDocumentStartJavaScript.kt | 18 +++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptBrowserPlugin.kt b/browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptBrowserPlugin.kt index f52b8789f8e8..0b55dca69c2c 100644 --- a/browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptBrowserPlugin.kt +++ b/browser-api/src/main/java/com/duckduckgo/browser/api/AddDocumentStartJavaScriptBrowserPlugin.kt @@ -16,8 +16,17 @@ package com.duckduckgo.browser.api +import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScript +/** +* Interface to provide implementations of [AddDocumentStartJavaScript] to the browser, through +* [PluginPoint]<[AddDocumentStartJavaScript]> + */ interface AddDocumentStartJavaScriptBrowserPlugin { + /** + * Provides an implementation of [AddDocumentStartJavaScript] to be used by the browser. + * @return an instance of [AddDocumentStartJavaScript] + */ fun addDocumentStartJavaScript(): AddDocumentStartJavaScript } diff --git a/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScript.kt b/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScript.kt index 2a7e040136d8..6411f73d7eda 100644 --- a/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScript.kt +++ b/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AddDocumentStartJavaScript.kt @@ -19,11 +19,19 @@ package com.duckduckgo.js.messaging.api import android.webkit.WebView /** - * Plugin interface for injecting JavaScript code that executes at document start. - * * Allows plugins to inject JavaScript that will be executed before any other scripts on the page. + * Interface for adding JavaScript code that executes at document start, before any other scripts on the page. * Useful for privacy protections and that need to run as early as possible and/or on iframes. */ interface AddDocumentStartJavaScript { + /** + * Adds JavaScript code into the provided [WebView] to be executed at document start. + * Notes: + * - If a different script already exists in this instance, it will be replaced. + * - It's not recommended to call this multiple times on the same WebView instance. + * If possible, we should rely on messaging to update the script behavior instead. + * + * @param webView the WebView where the script will be added + */ suspend fun addDocumentStartJavaScript(webView: WebView) val context: String @@ -52,12 +60,16 @@ interface AddDocumentStartJavaScriptScriptStrategy { */ val allowedOriginRules: Set + /** + * The context of the script + * @return context string + */ val context: String } interface AddDocumentStartScriptDelegate { /** - * Creates an AddDocumentStartJavaScriptPlugin implementation with the given [AddDocumentStartJavaScriptScriptStrategy]. + * Creates an [AddDocumentStartJavaScript] implementation with the given [AddDocumentStartJavaScriptScriptStrategy]. * @param strategy the strategy to use for determining injection behavior * @return [AddDocumentStartJavaScript] implementation */ From 19a98b9b1690939cc188ba1a41d261ae10b8bb50 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 2 Oct 2025 17:03:25 +0200 Subject: [PATCH 11/11] Add doc-bot file for addDocumentStartJavaScript --- .rules/add-document-start-javascript.md | 79 +++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .rules/add-document-start-javascript.md diff --git a/.rules/add-document-start-javascript.md b/.rules/add-document-start-javascript.md new file mode 100644 index 000000000000..94cf17890066 --- /dev/null +++ b/.rules/add-document-start-javascript.md @@ -0,0 +1,79 @@ +--- +title: "AddDocumentStartJavaScript" +description: "How we use AddDocumentStartJavaScript" +keywords: ["adddocumentstartjavascript", "addDocumentStartJavaScript", "AddDocumentStartJavaScript", "add document start javascript", "css", "c-s-s", "CSS", "C-S-S", "WebView", "webview", "contentscopescripts", "content-scope-scripts", "ContentScopeScripts", "contentScopeScripts"] +alwaysApply: false +--- + +# Adding a new script using `addDocumentStartJavaScript` + +There are 2 ways of adding a new script using +1. (Recommended) Using `AddDocumentStartScriptDelegate` to implement `AddDocumentStartJavaScript` via delegation pattern. This approach already takes care of +preventing adding the same script more than once, and removing the previous script and adding a new one if needed. It also adds checks on the WebView lifecycle to minimize native crashes. +2. (Manual approach, only recommended if more flexibility than the one provided by the delegate is needed) Manually implementing `AddDocumentStartJavaScript` + +## Using `AddDocumentStartScriptDelegate` to implement `AddDocumentStartJavaScript` via delegation pattern +``` +class AddDocumentStartJavaScript @Inject constructor( + scriptInjectorDelegate: AddDocumentStartScriptDelegate, +) : AddDocumentStartJavaScript by scriptInjectorDelegate.createPlugin( + object : AddDocumentStartJavaScriptScriptStrategy { + override suspend fun canInject(): Boolean { + TODO("Implement logic to determine if the script can be added (i.e. checking RC flags or user settings)" + + "or return true if always applicable") + } + + override suspend fun getScriptString(): String { + TODO("Return the script to be injected") + } + + override val allowedOriginRules: Set + get() = TODO("Return the set of allowed origin rules. For example:" + + "- if the script should be injected on all origins, return setOf(\"*\")" + + "- if the script should be injected only on specific origins, return setOf(\"https://example.com\", \"https://another.com\")" + + "- if the script should be injected on all subdomains of a domain, return setOf(\"https://*.example.com\")") + + override val context: String + get() = TODO("Return a string representing the context of this script, e.g., \"YourFeature\"") + }, +) +``` + +## Manually implementing `AddDocumentStartJavaScript` +Since the `AddDocumentStartScriptDelegate` already solves most of the issues and dangers of working with the `addDocumentStartJavaScript` API, manual implementation isn't recommended. If absolutely necessary, having a look at `RealAddDocumentStartScriptDelegate` is recommended, in order to replicate some best practices: +* If a script has already been added, don't add it again unless the content has changed +* If the content has changed, remove the previous `ScriptHandler` before adding the new script. Call `remove` on the main thread +* Use `WebViewCompatWrapper` instead of calling `WebViewCompat` directly, as it includes several checks on the `WebView` lifecycle and ensures proper threading is used + +# Adding a new script to the browser (DuckDuckGoWebView/BrowserTabFragment) + +If you need your script to be executed on the main browser WebView, you need to create a browser plugin that wraps your `AddDocumentStartJavaScript` implementation. + +## Step 1: Create the core implementation + +Follow the patterns described in the [delegation pattern section](#using-adddocumentstartscriptdelegate-to-implement-adddocumentstartjavascript-via-delegation-pattern) above. + +## Step 2: Create the browser plugin wrapper + +```kotlin +@ContributesMultibinding(FragmentScope::class) +class AddDocumentStartJavaScriptBrowserPlugin @Inject constructor( + private val AddDocumentStartJavaScript: AddDocumentStartJavaScript, +) : AddDocumentStartJavaScriptBrowserPlugin { + override fun addDocumentStartJavaScript(): AddDocumentStartJavaScript = + AddDocumentStartJavaScript +} +``` + +## How it works + +The browser automatically calls your script's `addDocumentStartJavaScript(webView)` method when: +- A new page loads +- If you also need it to be called when Privacy protections are updated, you need to add your implementation context to `BrowserTabViewModel#privacyProtectionsUpdated` +- If you need your script to be re-added in any other circumstances, follow the same pattern as `BrowserTabViewModel#privacyProtectionsUpdated`, so we only update the necessary scripts, not all of them. However, this is strongly discouraged by the Chromium team, and should only be done if messaging is not viable (for example, out of performance concerns) + +The `AddDocumentStartScriptDelegate` handles lifecycle management, script deduplication, and WebView safety checks. + +## Important Notes + +- Use appropriate scoping and consider using `@SingleInstanceIn()` with appropriate scoping to make sure only one instance of `WebMessaging` exists per `WebView`