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 ffce07c3ad9e..a11b25da64e1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -295,6 +295,7 @@ import com.duckduckgo.duckchat.api.inputscreen.InputScreenActivityResultCodes import com.duckduckgo.duckchat.api.inputscreen.InputScreenActivityResultParams import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.duckplayer.api.DuckPlayerSettingsNoParams +import com.duckduckgo.js.messaging.api.AdsjsMessaging import com.duckduckgo.js.messaging.api.JsCallbackData import com.duckduckgo.js.messaging.api.JsMessageCallback import com.duckduckgo.js.messaging.api.JsMessaging @@ -511,6 +512,10 @@ class BrowserTabFragment : @Named("DuckPlayer") lateinit var duckPlayerScripts: JsMessaging + @Inject + @Named("AdsjsContentScopeScripts") + lateinit var adsJsContentScopeScripts: AdsjsMessaging + @Inject lateinit var webContentDebugging: WebContentDebugging @@ -3006,7 +3011,25 @@ class BrowserTabFragment : webView?.let { it.isSafeWebViewEnabled = safeWebViewFeature.self().isEnabled() it.webViewClient = webViewClient - webViewClient.triggerJSInit(it) + lifecycleScope.launch(dispatchers.main()) { + webViewClient.triggerJSInit(it) + adsJsContentScopeScripts.register( + it, + object : JsMessageCallback() { + override fun process( + featureName: String, + method: String, + id: String?, + data: JSONObject?, + ) { + viewModel.processJsCallbackMessage(featureName, method, id, data, isActiveCustomTab()) { + it.url + } + } + }, + ) + } + it.webChromeClient = webChromeClient it.clearSslPreferences() diff --git a/content-scope-scripts/content-scope-scripts-api/build.gradle b/content-scope-scripts/content-scope-scripts-api/build.gradle index 03b1f51b57e4..53890368d876 100644 --- a/content-scope-scripts/content-scope-scripts-api/build.gradle +++ b/content-scope-scripts/content-scope-scripts-api/build.gradle @@ -23,6 +23,7 @@ apply from: "$rootProject.projectDir/gradle/android-library.gradle" dependencies { implementation AndroidX.core.ktx + implementation AndroidX.webkit implementation project(':feature-toggles-api') implementation project(':js-messaging-api') } diff --git a/content-scope-scripts/content-scope-scripts-api/src/main/java/com/duckduckgo/contentscopescripts/api/AdsjsContentScopeJsMessageHandlersPlugin.kt b/content-scope-scripts/content-scope-scripts-api/src/main/java/com/duckduckgo/contentscopescripts/api/AdsjsContentScopeJsMessageHandlersPlugin.kt new file mode 100644 index 000000000000..82556b4a2739 --- /dev/null +++ b/content-scope-scripts/content-scope-scripts-api/src/main/java/com/duckduckgo/contentscopescripts/api/AdsjsContentScopeJsMessageHandlersPlugin.kt @@ -0,0 +1,29 @@ +/* + * 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.contentscopescripts.api + +import com.duckduckgo.js.messaging.api.AdsjsMessageHandler + +/** + * Implement this interface and contribute it as a multibinding to manage JS Messages that are sent to C-S-S + */ +interface AdsjsContentScopeJsMessageHandlersPlugin { + /** + * @return a [AdsjsMessageHandler] that will be used to handle the JS messages + */ + fun getJsMessageHandler(): AdsjsMessageHandler +} diff --git a/content-scope-scripts/content-scope-scripts-api/src/main/java/com/duckduckgo/contentscopescripts/api/GlobalContentScopeJsMessageHandlersPlugin.kt b/content-scope-scripts/content-scope-scripts-api/src/main/java/com/duckduckgo/contentscopescripts/api/GlobalContentScopeJsMessageHandlersPlugin.kt new file mode 100644 index 000000000000..003384716ac1 --- /dev/null +++ b/content-scope-scripts/content-scope-scripts-api/src/main/java/com/duckduckgo/contentscopescripts/api/GlobalContentScopeJsMessageHandlersPlugin.kt @@ -0,0 +1,52 @@ +/* + * 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.contentscopescripts.api + +import com.duckduckgo.js.messaging.api.JsMessage +import com.duckduckgo.js.messaging.api.JsMessageCallback + +/** + * Plugin interface for global message handlers that should always be processed + * regardless of whether a specific feature handler matches the message. + * * Examples: addDebugFlag. + */ +interface GlobalContentScopeJsMessageHandlersPlugin { + + /** + * @return a [GlobalJsMessageHandler] that will be used to handle global messages + */ + fun getGlobalJsMessageHandler(): GlobalJsMessageHandler +} + +/** + * Handler for global messages that should be processed for all features. + */ +interface GlobalJsMessageHandler { + + /** + * Processes a global message. + */ + fun process( + jsMessage: JsMessage, + jsMessageCallback: JsMessageCallback, + ) + + /** + * Method this handler can process. + */ + val method: String +} diff --git a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/AdsjsContentScopeConfigPluginPoint.kt b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/AdsjsContentScopeConfigPluginPoint.kt new file mode 100644 index 000000000000..a25b66b236ff --- /dev/null +++ b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/AdsjsContentScopeConfigPluginPoint.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 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.contentscopescripts.impl + +import com.duckduckgo.anvil.annotations.ContributesPluginPoint +import com.duckduckgo.contentscopescripts.api.AdsjsContentScopeJsMessageHandlersPlugin +import com.duckduckgo.di.scopes.AppScope + +@ContributesPluginPoint( + scope = AppScope::class, + boundType = AdsjsContentScopeJsMessageHandlersPlugin::class, +) +@Suppress("unused") +interface AdsjsContentScopeJsMessageHandlersPluginPoint diff --git a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/GlobalContentScopeJsMessageHandlersPluginPoint.kt b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/GlobalContentScopeJsMessageHandlersPluginPoint.kt new file mode 100644 index 000000000000..b969c11e8e3b --- /dev/null +++ b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/GlobalContentScopeJsMessageHandlersPluginPoint.kt @@ -0,0 +1,28 @@ +/* + * 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.contentscopescripts.impl + +import com.duckduckgo.anvil.annotations.ContributesPluginPoint +import com.duckduckgo.contentscopescripts.api.GlobalContentScopeJsMessageHandlersPlugin +import com.duckduckgo.di.scopes.AppScope + +@ContributesPluginPoint( + scope = AppScope::class, + boundType = GlobalContentScopeJsMessageHandlersPlugin::class, +) +@Suppress("unused") +interface GlobalContentScopeJsMessageHandlersPluginPoint diff --git a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/messaging/AdsjsContentScopeMessaging.kt b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/messaging/AdsjsContentScopeMessaging.kt new file mode 100644 index 000000000000..a9c01ca58c4d --- /dev/null +++ b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/messaging/AdsjsContentScopeMessaging.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 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.contentscopescripts.impl.messaging + +import android.annotation.SuppressLint +import android.webkit.WebView +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature +import com.duckduckgo.common.utils.plugins.PluginPoint +import com.duckduckgo.contentscopescripts.api.AdsjsContentScopeJsMessageHandlersPlugin +import com.duckduckgo.contentscopescripts.api.GlobalContentScopeJsMessageHandlersPlugin +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.js.messaging.api.AdsjsMessaging +import com.duckduckgo.js.messaging.api.JsMessage +import com.duckduckgo.js.messaging.api.JsMessageCallback +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.moshi.Moshi +import javax.inject.Inject +import javax.inject.Named +import logcat.LogPriority.ERROR +import logcat.asLog +import logcat.logcat + +@ContributesBinding(ActivityScope::class) +@Named("AdsjsContentScopeScripts") +class AdsjsContentScopeMessaging @Inject constructor( + private val handlers: PluginPoint, + private val globalHandlers: PluginPoint, +) : AdsjsMessaging { + + private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build() + + private lateinit var webView: WebView + + override val context: String = "contentScopeScripts" + override val allowedDomains: Set = setOf("*") + + private fun process( + message: String, + jsMessageCallback: JsMessageCallback, + ) { + try { + val adapter = moshi.adapter(JsMessage::class.java) + val jsMessage = adapter.fromJson(message) + + jsMessage?.let { + if (context == jsMessage.context) { + // Process global handlers first (always processed regardless of feature handlers) + globalHandlers.getPlugins() + .map { it.getGlobalJsMessageHandler() } + .filter { it.method == jsMessage.method } + .forEach { handler -> + handler.process(jsMessage, jsMessageCallback) + } + + // Process with feature handlers + handlers.getPlugins().map { it.getJsMessageHandler() }.firstOrNull { + it.methods.contains(jsMessage.method) && it.featureName == jsMessage.featureName + }?.process(jsMessage, jsMessageCallback) + } + } + } catch (e: Exception) { + logcat(ERROR) { "Exception is ${e.asLog()}" } + } + } + + // TODO: A/B this, don't register if the feature is not enabled + @SuppressLint("AddWebMessageListenerUsage") // safeAddWebMessageListener belongs to app module + override fun register(webView: WebView, jsMessageCallback: JsMessageCallback?) { + if (jsMessageCallback == null) throw Exception("Callback cannot be null") + this.webView = webView + + runCatching { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + WebViewCompat.addWebMessageListener( + webView, + "contentScopeAdsjs", + allowedDomains, + ) { _, message, _, _, replyProxy -> + process( + message.data ?: "", + jsMessageCallback, + ) + } + true + } else { + false + } + }.getOrElse { exception -> + logcat(ERROR) { "Error adding WebMessageListener for contentScopeAdsjs: ${exception.asLog()}" } + false + } + } +} diff --git a/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/messaging/DebugFlagGlobalHandler.kt b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/messaging/DebugFlagGlobalHandler.kt new file mode 100644 index 000000000000..066b3c002248 --- /dev/null +++ b/content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/messaging/DebugFlagGlobalHandler.kt @@ -0,0 +1,50 @@ +/* + * 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.contentscopescripts.impl.messaging + +import com.duckduckgo.contentscopescripts.api.GlobalContentScopeJsMessageHandlersPlugin +import com.duckduckgo.contentscopescripts.api.GlobalJsMessageHandler +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.js.messaging.api.JsMessage +import com.duckduckgo.js.messaging.api.JsMessageCallback +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject +import logcat.logcat + +@ContributesMultibinding(AppScope::class) +class DebugFlagGlobalHandler @Inject constructor() : GlobalContentScopeJsMessageHandlersPlugin { + + override fun getGlobalJsMessageHandler(): GlobalJsMessageHandler = object : GlobalJsMessageHandler { + + override fun process( + jsMessage: JsMessage, + jsMessageCallback: JsMessageCallback, + ) { + if (jsMessage.method == method) { + logcat { "DebugFlagGlobalHandler addDebugFlag: ${jsMessage.featureName}" } + jsMessageCallback.process( + featureName = jsMessage.featureName, + method = jsMessage.method, + id = jsMessage.id, + data = jsMessage.params, + ) + } + } + + override val method: String = "addDebugFlag" + } +} diff --git a/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AdsjsMessaging.kt b/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AdsjsMessaging.kt new file mode 100644 index 000000000000..7adb6ddf831a --- /dev/null +++ b/js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AdsjsMessaging.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 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.api + +import android.webkit.WebView + +interface AdsjsMessaging { + + /** + * Method to register the JS interface to the webView instance + */ + fun register(webView: WebView, jsMessageCallback: JsMessageCallback?) + + /** + * Context name + */ + val context: String + + /** + * List of domains where the interface can be added + */ + val allowedDomains: Set +} + +interface AdsjsMessageHandler { + /** + * This method processes a [JsMessage] + */ + fun process( + jsMessage: JsMessage, + jsMessageCallback: JsMessageCallback?, + ) + + /** + * Name of the feature + */ + val featureName: String + + /** + * List of the methods the handler can handle + */ + val methods: List +}