Skip to content

Commit 06fdfe5

Browse files
committed
use existing capabilityChecker and DuckDuckGoWebView extension
1 parent 2252756 commit 06fdfe5

File tree

10 files changed

+153
-112
lines changed

10 files changed

+153
-112
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import com.duckduckgo.app.browser.SSLErrorType.NONE
108108
import com.duckduckgo.app.browser.WebViewErrorResponse.LOADING
109109
import com.duckduckgo.app.browser.WebViewErrorResponse.OMITTED
110110
import com.duckduckgo.app.browser.animations.ExperimentTrackersAnimationHelper
111+
import com.duckduckgo.app.browser.api.DuckDuckGoWebView
111112
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker
112113
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability
113114
import com.duckduckgo.app.browser.applinks.AppLinksLauncher
@@ -3206,7 +3207,6 @@ class BrowserTabFragment :
32063207
WebViewCompat.addDocumentStartJavaScript(webView, script, setOf("*"))
32073208

32083209
webView.safeAddWebMessageListener(
3209-
webViewCapabilityChecker,
32103210
"ddgBlobDownloadObj",
32113211
setOf("*"),
32123212
object : WebViewCompat.WebMessageListener {

app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt renamed to app/src/main/java/com/duckduckgo/app/browser/RealDuckDuckGoWebView.kt

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,17 @@ import android.webkit.WebViewClient
3333
import androidx.core.view.NestedScrollingChild3
3434
import androidx.core.view.NestedScrollingChildHelper
3535
import androidx.core.view.ViewCompat
36+
import androidx.webkit.ScriptHandler
3637
import androidx.webkit.WebViewCompat
3738
import androidx.webkit.WebViewCompat.WebMessageListener
39+
import com.duckduckgo.anvil.annotations.InjectWith
40+
import com.duckduckgo.app.browser.api.DuckDuckGoWebView
3841
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker
3942
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability
4043
import com.duckduckgo.app.browser.navigation.safeCopyBackForwardList
44+
import com.duckduckgo.contentscopescripts.impl.WebViewCompatWrapper
45+
import com.duckduckgo.di.scopes.ViewScope
46+
import javax.inject.Inject
4147
import logcat.LogPriority.ERROR
4248
import logcat.asLog
4349
import logcat.logcat
@@ -49,7 +55,9 @@ import logcat.logcat
4955
*
5056
* Originally based on https://github.com/takahirom/webview-in-coordinatorlayout for scrolling behaviour
5157
*/
52-
class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
58+
59+
@InjectWith(ViewScope::class)
60+
class RealDuckDuckGoWebView : DuckDuckGoWebView, NestedScrollingChild3 {
5361
private var lastClampedTopY: Boolean = true // when created we are always at the top
5462
private var contentAllowsSwipeToRefresh: Boolean = true
5563
private var enableSwipeRefreshCallback: ((Boolean) -> Unit)? = null
@@ -65,7 +73,13 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
6573
private val helper = CoordinatorLayoutHelper()
6674

6775
private var isDestroyed: Boolean = false
68-
var isSafeWebViewEnabled: Boolean = false
76+
override var isSafeWebViewEnabled: Boolean = false
77+
78+
@Inject
79+
lateinit var webViewCompatWrapper: WebViewCompatWrapper
80+
81+
@Inject
82+
lateinit var webViewCapabilityChecker: WebViewCapabilityChecker
6983

7084
constructor(context: Context) : this(context, null)
7185
constructor(
@@ -189,29 +203,29 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
189203
return super.getUrl()
190204
}
191205

192-
fun safeCopyBackForwardList(): WebBackForwardList? {
206+
override fun safeCopyBackForwardList(): WebBackForwardList? {
193207
if (isDestroyed) return null
194208
return (this as WebView).safeCopyBackForwardList()
195209
}
196210

197-
fun createSafePrintDocumentAdapter(documentName: String): PrintDocumentAdapter? {
211+
override fun createSafePrintDocumentAdapter(documentName: String): PrintDocumentAdapter? {
198212
if (isDestroyed) return null
199213
return createPrintDocumentAdapter(documentName)
200214
}
201215

202-
val safeSettings: WebSettings?
216+
override val safeSettings: WebSettings?
203217
get() {
204218
if (isDestroyed) return null
205219
return getSettings()
206220
}
207221

208-
val safeHitTestResult: HitTestResult?
222+
override val safeHitTestResult: HitTestResult?
209223
get() {
210224
if (isDestroyed) return null
211225
return getHitTestResult()
212226
}
213227

214-
fun setBottomMatchingBehaviourEnabled(value: Boolean) {
228+
override fun setBottomMatchingBehaviourEnabled(value: Boolean) {
215229
helper.setBottomMatchingBehaviourEnabled(value)
216230
}
217231

@@ -393,11 +407,11 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
393407
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY)
394408
}
395409

396-
fun setEnableSwipeRefreshCallback(callback: (Boolean) -> Unit) {
410+
override fun setEnableSwipeRefreshCallback(callback: (Boolean) -> Unit) {
397411
enableSwipeRefreshCallback = callback
398412
}
399413

400-
fun removeEnableSwipeRefreshCallback() {
414+
override fun removeEnableSwipeRefreshCallback() {
401415
enableSwipeRefreshCallback = null
402416
}
403417

@@ -417,8 +431,7 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
417431
}
418432

419433
@SuppressLint("RequiresFeature", "AddWebMessageListenerUsage")
420-
suspend fun safeAddWebMessageListener(
421-
webViewCapabilityChecker: WebViewCapabilityChecker,
434+
override suspend fun safeAddWebMessageListener(
422435
jsObjectName: String,
423436
allowedOriginRules: Set<String>,
424437
listener: WebMessageListener,
@@ -439,23 +452,18 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
439452
false
440453
}
441454

442-
@SuppressLint("RequiresFeature", "RemoveWebMessageListenerUsage")
443-
suspend fun safeRemoveWebMessageListener(
444-
webViewCapabilityChecker: WebViewCapabilityChecker,
445-
jsObjectName: String,
446-
): Boolean = runCatching {
447-
if (webViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener) && !isDestroyed) {
448-
WebViewCompat.removeWebMessageListener(
449-
this,
450-
jsObjectName,
451-
)
452-
true
453-
} else {
454-
false
455-
}
456-
}.getOrElse { exception ->
457-
logcat(ERROR) { "Error removing WebMessageListener: $jsObjectName: ${exception.asLog()}" }
458-
false
455+
@SuppressLint("RequiresFeature")
456+
override suspend fun safeAddDocumentStartJavaScript(
457+
script: String,
458+
allowedOriginRules: Set<String>,
459+
): ScriptHandler? {
460+
return runCatching {
461+
if (webViewCapabilityChecker.isSupported(WebViewCapability.DocumentStartJavaScript) && !isDestroyed) {
462+
webViewCompatWrapper.addDocumentStartJavaScript(this, script, allowedOriginRules)
463+
} else {
464+
null
465+
}
466+
}.getOrElse { null }
459467
}
460468

461469
companion object {

app/src/main/res/layout/include_duckduckgo_browser_webview.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
~ limitations under the License.
1515
-->
1616

17-
<com.duckduckgo.app.browser.DuckDuckGoWebView xmlns:android="http://schemas.android.com/apk/res/android"
17+
<com.duckduckgo.app.browser.RealDuckDuckGoWebView xmlns:android="http://schemas.android.com/apk/res/android"
1818
xmlns:app="http://schemas.android.com/apk/res-auto"
1919
xmlns:tools="http://schemas.android.com/tools"
2020
android:id="@+id/browserWebView"
@@ -34,4 +34,4 @@
3434
app:layout_behavior="@string/appbar_scrolling_view_behavior"
3535
tools:visibility="visible">
3636

37-
</com.duckduckgo.app.browser.DuckDuckGoWebView>
37+
</com.duckduckgo.app.browser.RealDuckDuckGoWebView>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.duckduckgo.app.browser
2+
3+
import androidx.test.ext.junit.runners.AndroidJUnit4
4+
import androidx.test.platform.app.InstrumentationRegistry
5+
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker
6+
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability.DocumentStartJavaScript
7+
import com.duckduckgo.contentscopescripts.impl.WebViewCompatWrapper
8+
import kotlinx.coroutines.test.runTest
9+
import org.junit.Before
10+
import org.junit.Test
11+
import org.junit.runner.RunWith
12+
import org.mockito.Mockito.mock
13+
import org.mockito.Mockito.verify
14+
import org.mockito.kotlin.never
15+
import org.mockito.kotlin.whenever
16+
17+
@RunWith(AndroidJUnit4::class)
18+
class RealDuckDuckGoWebViewTest {
19+
20+
val testee: RealDuckDuckGoWebView = RealDuckDuckGoWebView(InstrumentationRegistry.getInstrumentation().targetContext)
21+
22+
private val mockWebViewCapabilityChecker: WebViewCapabilityChecker = mock()
23+
private val mockWebViewCompatWrapper: WebViewCompatWrapper = mock()
24+
25+
@Before
26+
fun setUp() {
27+
testee.webViewCompatWrapper = mockWebViewCompatWrapper
28+
}
29+
30+
@Test
31+
fun whenSafeAddDocumentStartJavaScriptWithFeatureEnabledThenAddScript() = runTest {
32+
whenever(mockWebViewCapabilityChecker.isSupported(DocumentStartJavaScript)).thenReturn(true)
33+
34+
testee.safeAddDocumentStartJavaScript("script", setOf("*"))
35+
36+
verify(mockWebViewCompatWrapper).addDocumentStartJavaScript(testee, "script", setOf("*"))
37+
}
38+
39+
@Test
40+
fun whenSafeAddDocumentStartJavaScriptWithFeatureDisabledThenDoNotAddScript() = runTest {
41+
whenever(mockWebViewCapabilityChecker.isSupported(DocumentStartJavaScript)).thenReturn(false)
42+
43+
testee.safeAddDocumentStartJavaScript("script", setOf("*"))
44+
45+
verify(mockWebViewCompatWrapper, never()).addDocumentStartJavaScript(testee, "script", setOf("*"))
46+
}
47+
}

browser-api/build.gradle

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

3233
implementation "com.squareup.logcat:logcat:_"
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.api
18+
19+
import android.annotation.SuppressLint
20+
import android.content.Context
21+
import android.print.PrintDocumentAdapter
22+
import android.util.AttributeSet
23+
import android.webkit.WebBackForwardList
24+
import android.webkit.WebSettings
25+
import android.webkit.WebView
26+
import androidx.webkit.ScriptHandler
27+
import androidx.webkit.WebViewCompat.WebMessageListener
28+
29+
abstract class DuckDuckGoWebView(
30+
context: Context,
31+
attrs: AttributeSet?,
32+
) : WebView(context, attrs) {
33+
abstract fun removeEnableSwipeRefreshCallback()
34+
abstract fun safeCopyBackForwardList(): WebBackForwardList?
35+
abstract fun createSafePrintDocumentAdapter(documentName: String): PrintDocumentAdapter?
36+
abstract val safeSettings: WebSettings?
37+
abstract val safeHitTestResult: HitTestResult?
38+
abstract fun setBottomMatchingBehaviourEnabled(value: Boolean)
39+
abstract var isSafeWebViewEnabled: Boolean
40+
abstract fun setEnableSwipeRefreshCallback(callback: (Boolean) -> Unit)
41+
42+
@SuppressLint("RequiresFeature", "AddWebMessageListenerUsage")
43+
abstract suspend fun safeAddWebMessageListener(
44+
jsObjectName: String,
45+
allowedOriginRules: Set<String>,
46+
listener: WebMessageListener,
47+
): Boolean
48+
49+
abstract suspend fun safeAddDocumentStartJavaScript(
50+
script: String,
51+
allowedOriginRules: Set<String>,
52+
): ScriptHandler?
53+
}

content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/ContentScopeScriptsJsInjectorPlugin.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.duckduckgo.contentscopescripts.impl
1919
import android.annotation.SuppressLint
2020
import android.webkit.WebView
2121
import androidx.webkit.ScriptHandler
22+
import com.duckduckgo.app.browser.api.DuckDuckGoWebView
2223
import com.duckduckgo.browser.api.JsInjectorPlugin
2324
import com.duckduckgo.common.utils.DispatcherProvider
2425
import com.duckduckgo.contentscopescripts.api.contentscopeExperiments.ContentScopeExperiments
@@ -34,7 +35,6 @@ class ContentScopeScriptsJsInjectorPlugin @Inject constructor(
3435
private val adsJsContentScopeScripts: AdsJsContentScopeScripts,
3536
private val contentScopeExperiments: ContentScopeExperiments,
3637
private val dispatcherProvider: DispatcherProvider,
37-
private val webViewCompatWrapper: WebViewCompatWrapper,
3838
) : JsInjectorPlugin {
3939
private var script: ScriptHandler? = null
4040
private var currentScriptString: String? = null
@@ -48,9 +48,6 @@ class ContentScopeScriptsJsInjectorPlugin @Inject constructor(
4848
activeExperiments = withContext(dispatcherProvider.io()) { contentScopeExperiments.getActiveExperiments() }
4949

5050
withContext(dispatcherProvider.main()) {
51-
if (!webViewCompatWrapper.isDocumentStartScriptSupported()) {
52-
return@withContext
53-
}
5451
val scriptString = adsJsContentScopeScripts.getScript(activeExperiments)
5552
if (scriptString == currentScriptString) {
5653
return@withContext
@@ -60,8 +57,13 @@ class ContentScopeScriptsJsInjectorPlugin @Inject constructor(
6057
script = null
6158
}
6259
if (adsJsContentScopeScripts.isEnabled()) {
63-
currentScriptString = scriptString
64-
script = webViewCompatWrapper.addDocumentStartJavaScript(webView, scriptString, setOf("*"))
60+
(webView as? DuckDuckGoWebView)?.safeAddDocumentStartJavaScript(
61+
scriptString,
62+
setOf("*"),
63+
)?.let {
64+
script = it
65+
currentScriptString = scriptString
66+
}
6567
}
6668
}
6769
}

content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/RealWebViewCompatWrapper.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 DuckDuckGo
2+
* Copyright (c) 2025 DuckDuckGo
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,18 +19,13 @@ package com.duckduckgo.contentscopescripts.impl
1919
import android.annotation.SuppressLint
2020
import androidx.webkit.ScriptHandler
2121
import androidx.webkit.WebViewCompat
22-
import androidx.webkit.WebViewFeature
2322
import com.duckduckgo.di.scopes.AppScope
2423
import com.squareup.anvil.annotations.ContributesBinding
2524
import javax.inject.Inject
2625

2726
@SuppressLint("RequiresFeature")
2827
@ContributesBinding(AppScope::class)
2928
class RealWebViewCompatWrapper @Inject constructor() : WebViewCompatWrapper {
30-
override fun isDocumentStartScriptSupported(): Boolean {
31-
return WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)
32-
}
33-
3429
override fun addDocumentStartJavaScript(
3530
webView: android.webkit.WebView,
3631
script: String,

content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/WebViewCompatWrapper.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import androidx.webkit.ScriptHandler
2121

2222
interface WebViewCompatWrapper {
2323

24-
fun isDocumentStartScriptSupported(): Boolean
25-
2624
fun addDocumentStartJavaScript(
2725
webView: WebView,
2826
script: String,

0 commit comments

Comments
 (0)