Skip to content

Add support to receive messages #6601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: cris/adsjs/support-adsjs
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -511,6 +512,10 @@ class BrowserTabFragment :
@Named("DuckPlayer")
lateinit var duckPlayerScripts: JsMessaging

@Inject
@Named("AdsjsContentScopeScripts")
lateinit var adsJsContentScopeScripts: AdsjsMessaging

@Inject
lateinit var webContentDebugging: WebContentDebugging

Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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<AdsjsContentScopeJsMessageHandlersPlugin>,
private val globalHandlers: PluginPoint<GlobalContentScopeJsMessageHandlersPlugin>,
) : AdsjsMessaging {

private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build()

private lateinit var webView: WebView

override val context: String = "contentScopeScripts"
override val allowedDomains: Set<String> = 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
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading
Loading