From c6862207c353436776f74cd6e94de35e671bd47c Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 30 Oct 2025 12:49:56 +0100 Subject: [PATCH 1/4] Configure script based on remote config --- .../app/browser/BrowserTabFragment.kt | 35 ++++++++++++++++ .../browser/webview/WebViewCompatFeature.kt | 42 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/webview/WebViewCompatFeature.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 9ee3f411d9b0..b403325b49b3 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -182,6 +182,8 @@ import com.duckduckgo.app.browser.webshare.WebShareChooser import com.duckduckgo.app.browser.webshare.WebViewCompatWebShareChooser import com.duckduckgo.app.browser.webview.WebContentDebugging import com.duckduckgo.app.browser.webview.WebViewBlobDownloadFeature +import com.duckduckgo.app.browser.webview.WebViewCompatFeature +import com.duckduckgo.app.browser.webview.WebViewCompatFeatureSettings import com.duckduckgo.app.browser.webview.safewebview.SafeWebViewFeature import com.duckduckgo.app.cta.ui.BrokenSitePromptDialogCta import com.duckduckgo.app.cta.ui.Cta @@ -308,6 +310,7 @@ import com.duckduckgo.js.messaging.api.JsCallbackData import com.duckduckgo.js.messaging.api.JsMessageCallback import com.duckduckgo.js.messaging.api.JsMessaging import com.duckduckgo.js.messaging.api.SubscriptionEventData +import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams import com.duckduckgo.navigation.api.GlobalActivityStarter @@ -343,6 +346,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob @@ -599,6 +604,9 @@ class BrowserTabFragment : @Inject lateinit var omnibarFeatureRepository: OmnibarFeatureRepository + @Inject + lateinit var webViewCompatFeature: WebViewCompatFeature + /** * We use this to monitor whether the user was seeing the in-context Email Protection signup prompt * This is needed because the activity stack will be cleared if an external link is opened in our browser @@ -3246,6 +3254,7 @@ class BrowserTabFragment : onInContextEmailProtectionSignupPromptShown = { showNativeInContextEmailProtectionSignupPrompt() }, ) configureWebViewForBlobDownload(it) + configureWebViewForWebViewCompatTest(it) configureWebViewForAutofill(it) printInjector.addJsInterface(it) { viewModel.printFromWebView() } autoconsent.addJsInterface(it, autoconsentCallback) @@ -3368,6 +3377,32 @@ class BrowserTabFragment : daxDialogIntroBubble.root.gone() } + private var proxy: JavaScriptReplyProxy? = null + + private val delay = "\$DELAY$" + private val postInitialPing = "\$POST_INITIAL_PING$" + private val replyToNativeMessages = "\$REPLY_TO_NATIVE_MESSAGES$" + + private fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView) { + lifecycleScope.launch(dispatchers.main()) { + val script = withContext(dispatchers.io()) { + if (!webViewCompatFeature.self().isEnabled()) return@withContext null + + val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + val adapter = moshi.adapter(WebViewCompatFeatureSettings::class.java) + val webViewCompatSettings = webViewCompatFeature.self().getSettings()?.let { + adapter.fromJson(it) + } + context?.resources?.openRawResource(R.raw.webviewcompat_test_script)?.bufferedReader().use { it?.readText() }.orEmpty() + .replace(delay, webViewCompatSettings?.jsInitialPingDelay?.toString() ?: "0") + .replace(postInitialPing, webViewCompatFeature.jsSendsInitialPing().isEnabled().toString()) + .replace(replyToNativeMessages, webViewCompatFeature.jsRepliesToNativeMessages().isEnabled().toString()) + } ?: return@launch + + webViewCompatWrapper.addDocumentStartJavaScript(webView, script, setOf("*")) + } + } + @SuppressLint("AddDocumentStartJavaScriptUsage") private fun configureWebViewForBlobDownload(webView: DuckDuckGoWebView) { lifecycleScope.launch(dispatchers.main()) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/webview/WebViewCompatFeature.kt b/app/src/main/java/com/duckduckgo/app/browser/webview/WebViewCompatFeature.kt new file mode 100644 index 000000000000..5c8d3c5f0c19 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/webview/WebViewCompatFeature.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 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.webview + +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue + +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "webViewCompat", +) +interface WebViewCompatFeature { + + @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + fun self(): Toggle + + @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + fun jsSendsInitialPing(): Toggle + + @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + fun jsRepliesToNativeMessages(): Toggle +} + +data class WebViewCompatFeatureSettings( + val jsInitialPingDelay: Long = 0, +) From bd97438255b4075db44ccef8e3f4b48ce43feb14 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 3 Nov 2025 14:51:09 +0100 Subject: [PATCH 2/4] Fix CI issues --- .../main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index b403325b49b3..69328bd2a167 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -310,7 +310,6 @@ import com.duckduckgo.js.messaging.api.JsCallbackData import com.duckduckgo.js.messaging.api.JsMessageCallback import com.duckduckgo.js.messaging.api.JsMessaging import com.duckduckgo.js.messaging.api.SubscriptionEventData -import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams import com.duckduckgo.navigation.api.GlobalActivityStarter From 5c9c119653502521d55697ba6b235ddc05b350b7 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 3 Nov 2025 15:11:47 +0100 Subject: [PATCH 3/4] Extract WebViewCompat test logic to another class --- .../app/browser/BrowserTabFragment.kt | 38 ++--------- .../app/browser/WebViewCompatTestHelper.kt | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/WebViewCompatTestHelper.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 69328bd2a167..d21141fccdc8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -182,8 +182,6 @@ import com.duckduckgo.app.browser.webshare.WebShareChooser import com.duckduckgo.app.browser.webshare.WebViewCompatWebShareChooser import com.duckduckgo.app.browser.webview.WebContentDebugging import com.duckduckgo.app.browser.webview.WebViewBlobDownloadFeature -import com.duckduckgo.app.browser.webview.WebViewCompatFeature -import com.duckduckgo.app.browser.webview.WebViewCompatFeatureSettings import com.duckduckgo.app.browser.webview.safewebview.SafeWebViewFeature import com.duckduckgo.app.cta.ui.BrokenSitePromptDialogCta import com.duckduckgo.app.cta.ui.Cta @@ -331,7 +329,7 @@ import com.duckduckgo.savedsites.api.models.SavedSitesNames import com.duckduckgo.savedsites.impl.bookmarks.BookmarksBottomSheetDialog import com.duckduckgo.savedsites.impl.bookmarks.FaviconPromptSheet import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment -import com.duckduckgo.serp.logos.api.SerpLogoScreens.* +import com.duckduckgo.serp.logos.api.SerpLogoScreens.EasterEggLogoScreen import com.duckduckgo.serp.logos.api.SerpLogos import com.duckduckgo.site.permissions.api.SitePermissionsDialogLauncher import com.duckduckgo.site.permissions.api.SitePermissionsGrantedListener @@ -345,8 +343,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob @@ -604,7 +600,7 @@ class BrowserTabFragment : lateinit var omnibarFeatureRepository: OmnibarFeatureRepository @Inject - lateinit var webViewCompatFeature: WebViewCompatFeature + lateinit var webViewCompatTestHelper: WebViewCompatTestHelper /** * We use this to monitor whether the user was seeing the in-context Email Protection signup prompt @@ -3253,7 +3249,9 @@ class BrowserTabFragment : onInContextEmailProtectionSignupPromptShown = { showNativeInContextEmailProtectionSignupPrompt() }, ) configureWebViewForBlobDownload(it) - configureWebViewForWebViewCompatTest(it) + lifecycleScope.launch { + webViewCompatTestHelper.configureWebViewForWebViewCompatTest(it) + } configureWebViewForAutofill(it) printInjector.addJsInterface(it) { viewModel.printFromWebView() } autoconsent.addJsInterface(it, autoconsentCallback) @@ -3376,32 +3374,6 @@ class BrowserTabFragment : daxDialogIntroBubble.root.gone() } - private var proxy: JavaScriptReplyProxy? = null - - private val delay = "\$DELAY$" - private val postInitialPing = "\$POST_INITIAL_PING$" - private val replyToNativeMessages = "\$REPLY_TO_NATIVE_MESSAGES$" - - private fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView) { - lifecycleScope.launch(dispatchers.main()) { - val script = withContext(dispatchers.io()) { - if (!webViewCompatFeature.self().isEnabled()) return@withContext null - - val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() - val adapter = moshi.adapter(WebViewCompatFeatureSettings::class.java) - val webViewCompatSettings = webViewCompatFeature.self().getSettings()?.let { - adapter.fromJson(it) - } - context?.resources?.openRawResource(R.raw.webviewcompat_test_script)?.bufferedReader().use { it?.readText() }.orEmpty() - .replace(delay, webViewCompatSettings?.jsInitialPingDelay?.toString() ?: "0") - .replace(postInitialPing, webViewCompatFeature.jsSendsInitialPing().isEnabled().toString()) - .replace(replyToNativeMessages, webViewCompatFeature.jsRepliesToNativeMessages().isEnabled().toString()) - } ?: return@launch - - webViewCompatWrapper.addDocumentStartJavaScript(webView, script, setOf("*")) - } - } - @SuppressLint("AddDocumentStartJavaScriptUsage") private fun configureWebViewForBlobDownload(webView: DuckDuckGoWebView) { lifecycleScope.launch(dispatchers.main()) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebViewCompatTestHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/WebViewCompatTestHelper.kt new file mode 100644 index 000000000000..32d51d8a964b --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewCompatTestHelper.kt @@ -0,0 +1,66 @@ +/* + * 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.app.browser.webview.WebViewCompatFeature +import com.duckduckgo.app.browser.webview.WebViewCompatFeatureSettings +import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.moshi.Moshi +import dagger.SingleInstanceIn +import kotlinx.coroutines.withContext +import javax.inject.Inject + +private const val delay = "\$DELAY$" +private const val postInitialPing = "\$POST_INITIAL_PING$" +private const val replyToNativeMessages = "\$REPLY_TO_NATIVE_MESSAGES$" + +interface WebViewCompatTestHelper { + suspend fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView) +} + +@ContributesBinding(FragmentScope::class) +@SingleInstanceIn(FragmentScope::class) +class RealWebViewCompatTestHelper @Inject constructor( + private val dispatchers: DispatcherProvider, + private val webViewCompatFeature: WebViewCompatFeature, + private val webViewCompatWrapper: WebViewCompatWrapper, + moshi: Moshi, +) : WebViewCompatTestHelper { + + private val adapter = moshi.adapter(WebViewCompatFeatureSettings::class.java) + + override suspend fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView) { + withContext(dispatchers.main()) { + val script = withContext(dispatchers.io()) { + if (!webViewCompatFeature.self().isEnabled()) return@withContext null + + val webViewCompatSettings = webViewCompatFeature.self().getSettings()?.let { + adapter.fromJson(it) + } + webView.resources?.openRawResource(R.raw.webviewcompat_test_script)?.bufferedReader().use { it?.readText() }.orEmpty() + .replace(delay, webViewCompatSettings?.jsInitialPingDelay?.toString() ?: "0") + .replace(postInitialPing, webViewCompatFeature.jsSendsInitialPing().isEnabled().toString()) + .replace(replyToNativeMessages, webViewCompatFeature.jsRepliesToNativeMessages().isEnabled().toString()) + } ?: return@withContext + + webViewCompatWrapper.addDocumentStartJavaScript(webView, script, setOf("*")) + } + } +} From 759a26d1a7a2529cee0cdada38b1a69f07b96790 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Tue, 4 Nov 2025 11:10:30 +0100 Subject: [PATCH 4/4] Improve threading --- .../app/browser/WebViewCompatTestHelper.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebViewCompatTestHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/WebViewCompatTestHelper.kt index 32d51d8a964b..a0efa6193aa2 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewCompatTestHelper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewCompatTestHelper.kt @@ -47,19 +47,19 @@ class RealWebViewCompatTestHelper @Inject constructor( private val adapter = moshi.adapter(WebViewCompatFeatureSettings::class.java) override suspend fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView) { - withContext(dispatchers.main()) { - val script = withContext(dispatchers.io()) { - if (!webViewCompatFeature.self().isEnabled()) return@withContext null + val script = withContext(dispatchers.io()) { + if (!webViewCompatFeature.self().isEnabled()) return@withContext null - val webViewCompatSettings = webViewCompatFeature.self().getSettings()?.let { - adapter.fromJson(it) - } - webView.resources?.openRawResource(R.raw.webviewcompat_test_script)?.bufferedReader().use { it?.readText() }.orEmpty() - .replace(delay, webViewCompatSettings?.jsInitialPingDelay?.toString() ?: "0") - .replace(postInitialPing, webViewCompatFeature.jsSendsInitialPing().isEnabled().toString()) - .replace(replyToNativeMessages, webViewCompatFeature.jsRepliesToNativeMessages().isEnabled().toString()) - } ?: return@withContext + val webViewCompatSettings = webViewCompatFeature.self().getSettings()?.let { + adapter.fromJson(it) + } + webView.resources?.openRawResource(R.raw.webviewcompat_test_script)?.bufferedReader().use { it?.readText() }.orEmpty() + .replace(delay, webViewCompatSettings?.jsInitialPingDelay?.toString() ?: "0") + .replace(postInitialPing, webViewCompatFeature.jsSendsInitialPing().isEnabled().toString()) + .replace(replyToNativeMessages, webViewCompatFeature.jsRepliesToNativeMessages().isEnabled().toString()) + } ?: return + withContext(dispatchers.main()) { webViewCompatWrapper.addDocumentStartJavaScript(webView, script, setOf("*")) } }