Skip to content
Open
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 @@ -85,6 +85,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.Unavailab
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.SubscriptionEventData
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
import com.duckduckgo.privacy.config.api.AmpLinks
Expand All @@ -100,6 +101,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.json.JSONObject
import org.junit.Before
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -376,6 +378,17 @@ class BrowserWebViewClientTest {
assertFalse(fakeMessagingPlugins.plugin.registered)
}

@Test
fun whenPostMessageThenCallPostMessage() = runTest {
val data = SubscriptionEventData("feature", "method", JSONObject())

assertFalse(fakeMessagingPlugins.plugin.messagePosted)

testee.postMessage(data)

assertTrue(fakeMessagingPlugins.plugin.messagePosted)
}

@UiThreadTest
@Test
fun whenOnReceivedHttpAuthRequestThenListenerNotified() {
Expand Down Expand Up @@ -1337,6 +1350,9 @@ class BrowserWebViewClientTest {
var registered = false
private set

var messagePosted = false
private set

override fun unregister(webView: WebView) {
registered = false
}
Expand All @@ -1347,6 +1363,10 @@ class BrowserWebViewClientTest {
) {
registered = true
}

override fun postMessage(subscriptionEventData: SubscriptionEventData) {
messagePosted = true
}
}

class FakeWebMessagingPluginPoint : PluginPoint<WebMessagingPlugin> {
Expand Down
48 changes: 28 additions & 20 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1194,24 +1194,29 @@ class BrowserTabFragment :
private fun onOmnibarCustomTabPrivacyDashboardPressed() {
val params = PrivacyDashboardPrimaryScreen(tabId)
val intent = globalActivityStarter.startIntent(requireContext(), params)
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
postBreakageReportingEvent()
intent?.let { activityResultPrivacyDashboard.launch(intent) }
pixel.fire(CustomTabPixelNames.CUSTOM_TABS_PRIVACY_DASHBOARD_OPENED)
}

private fun postBreakageReportingEvent() {
val eventData = createBreakageReportingEventData()
webViewClient.postMessage(eventData)
}

private fun onFireButtonPressed() {
val isFocusedNtp = omnibar.viewMode == ViewMode.NewTab && omnibar.getText().isEmpty() && omnibar.omnibarTextInput.hasFocus()
browserActivity?.launchFire(launchedFromFocusedNtp = isFocusedNtp)
viewModel.onFireMenuSelected()
}

private fun onBrowserMenuButtonPressed() {
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
postBreakageReportingEvent()
viewModel.onBrowserMenuClicked(isCustomTab = isActiveCustomTab())
}

private fun onOmnibarPrivacyShieldButtonPressed() {
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
postBreakageReportingEvent()
viewModel.onOmnibarPrivacyShieldButtonPressed()
launchPrivacyDashboard(toggle = false)
}
Expand Down Expand Up @@ -1939,11 +1944,17 @@ class BrowserTabFragment :
is Command.ShowFireproofWebSiteConfirmation -> fireproofWebsiteConfirmation(it.fireproofWebsiteEntity)
is Command.DeleteFireproofConfirmation -> removeFireproofWebsiteConfirmation(it.fireproofWebsiteEntity)
is Command.RefreshAndShowPrivacyProtectionEnabledConfirmation -> {
webView?.let {
webViewClient.configureWebView(it, null)
}
refresh()
privacyProtectionEnabledConfirmation(it.domain)
}

is Command.RefreshAndShowPrivacyProtectionDisabledConfirmation -> {
webView?.let {
webViewClient.configureWebView(it, null)
}
refresh()
privacyProtectionDisabledConfirmation(it.domain)
}
Expand Down Expand Up @@ -3031,23 +3042,6 @@ class BrowserTabFragment :
webView?.let {
it.isSafeWebViewEnabled = safeWebViewFeature.self().isEnabled()
it.webViewClient = webViewClient
lifecycleScope.launch(dispatchers.main()) {
webViewClient.configureWebView(
it,
object : WebViewCompatMessageCallback {
override fun process(
featureName: String,
method: String,
id: String?,
data: JSONObject?,
onResponse: (JSONObject) -> Unit,
) {
viewModel.webViewCompatProcessJsCallbackMessage(featureName, method, id, data, onResponse)
}
},
)
}

it.webChromeClient = webChromeClient
it.clearSslPreferences()

Expand Down Expand Up @@ -3135,6 +3129,20 @@ class BrowserTabFragment :
}
},
)
webViewClient.configureWebView(
it,
object : WebViewCompatMessageCallback {
override fun process(
featureName: String,
method: String,
id: String?,
data: JSONObject?,
onResponse: (JSONObject) -> Unit,
) {
viewModel.webViewCompatProcessJsCallbackMessage(featureName, method, id, data, onResponse)
}
},
)
duckPlayerScripts.register(
it,
object : JsMessageCallback() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3687,6 +3687,19 @@ class BrowserTabViewModel @Inject constructor(
"addDebugFlag" -> {
site?.debugFlags = (site?.debugFlags ?: listOf()).toMutableList().plus(featureName)?.toList()
}
"breakageReportResult" -> if (data != null) {
breakageReportResult(data)
}
"initialPing" -> {
// TODO: Eventually, we might want plugins here
val response = JSONObject(
mapOf(
"desktopModeEnabled" to (getSite()?.isDesktopMode ?: false),
"forcedZoomEnabled" to (accessibilityViewState.value?.forceZoom ?: false),
),
)
onResponse(response)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On
import com.duckduckgo.duckplayer.impl.DUCK_PLAYER_OPEN_IN_YOUTUBE_PATH
import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin
import com.duckduckgo.js.messaging.api.SubscriptionEventData
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
Expand Down Expand Up @@ -468,13 +469,15 @@ class BrowserWebViewClient @Inject constructor(
webView.settings.mediaPlaybackRequiresUserGesture = mediaPlayback.doesMediaPlaybackRequireUserGestureForUrl(url)
}

fun configureWebView(webView: DuckDuckGoWebView, callback: WebViewCompatMessageCallback) {
fun configureWebView(webView: DuckDuckGoWebView, callback: WebViewCompatMessageCallback?) {
addDocumentStartJavascriptPlugins.getPlugins().forEach { plugin ->
plugin.addDocumentStartJavaScript(webView)
}

webMessagingPlugins.getPlugins().forEach { plugin ->
plugin.register(callback, webView)
callback?.let {
webMessagingPlugins.getPlugins().forEach { plugin ->
plugin.register(callback, webView)
}
}
}

Expand Down Expand Up @@ -761,6 +764,14 @@ class BrowserWebViewClient @Inject constructor(
plugin.unregister(webView)
}
}

fun postMessage(
eventData: SubscriptionEventData,
) {
webMessagingPlugins.getPlugins().forEach {
it.postMessage(eventData)
}
}
}

enum class WebViewPixelName(override val pixelName: String) : Pixel.PixelName {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.breakagereporting.impl

import com.duckduckgo.contentscopescripts.api.WebViewCompatContentScopeJsMessageHandlersPlugin
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.js.messaging.api.JsMessage
import com.duckduckgo.js.messaging.api.ProcessResult
import com.duckduckgo.js.messaging.api.ProcessResult.SendToConsumer
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject

@ContributesMultibinding(ActivityScope::class)
class WebViewCompatBreakageContentScopeJsMessageHandler @Inject constructor() : WebViewCompatContentScopeJsMessageHandlersPlugin {

override fun getJsMessageHandler(): WebViewCompatMessageHandler = object : WebViewCompatMessageHandler {

override fun process(
jsMessage: JsMessage,
): ProcessResult {
return SendToConsumer
}

override val featureName: String = "breakageReporting"
override val methods: List<String> = listOf("breakageReportResult")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.contentscopescripts.api.WebViewCompatContentScopeJsMessageHandlersPlugin
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.js.messaging.api.JsMessage
import com.duckduckgo.js.messaging.api.ProcessResult
import com.duckduckgo.js.messaging.api.ProcessResult.SendToConsumer
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject

@ContributesMultibinding(ActivityScope::class)
class WebViewCompatMessagingContentScopeJsMessageHandler @Inject constructor() : WebViewCompatContentScopeJsMessageHandlersPlugin {

override fun getJsMessageHandler(): WebViewCompatMessageHandler = object : WebViewCompatMessageHandler {

override fun process(
jsMessage: JsMessage,
): ProcessResult {
return SendToConsumer
}

override val featureName: String = "messaging"
override val methods: List<String> = listOf("initialPing")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ class ContentScopeScriptsJsMessaging @Inject constructor(
subscriptionEventData.subscriptionName,
subscriptionEventData.params,
)
jsMessageHelper.sendSubscriptionEvent(subscriptionEvent, callbackName, secret, webView)
if (::webView.isInitialized) {
jsMessageHelper.sendSubscriptionEvent(subscriptionEvent, callbackName, secret, webView)
}
}

override fun onResponse(response: JsCallbackData) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@ import com.duckduckgo.js.messaging.api.JsCallbackData
import com.duckduckgo.js.messaging.api.JsMessage
import com.duckduckgo.js.messaging.api.ProcessResult.SendResponse
import com.duckduckgo.js.messaging.api.ProcessResult.SendToConsumer
import com.duckduckgo.js.messaging.api.SubscriptionEvent
import com.duckduckgo.js.messaging.api.SubscriptionEventData
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
import com.squareup.anvil.annotations.ContributesMultibinding
import com.squareup.moshi.Moshi
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import logcat.LogPriority.ERROR
import logcat.asLog
import logcat.logcat
Expand All @@ -50,6 +53,7 @@ class ContentScopeScriptsWebMessagingPlugin @Inject constructor(
private val handlers: PluginPoint<WebViewCompatContentScopeJsMessageHandlersPlugin>,
private val globalHandlers: PluginPoint<GlobalContentScopeJsMessageHandlersPlugin>,
private val webViewCompatContentScopeScripts: WebViewCompatContentScopeScripts,
private val contentScopeScriptsJsMessaging: ContentScopeScriptsJsMessaging,
private val webViewCompatWrapper: WebViewCompatWrapper,
private val dispatcherProvider: DispatcherProvider,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
Expand All @@ -60,6 +64,8 @@ class ContentScopeScriptsWebMessagingPlugin @Inject constructor(
private val context: String = "contentScopeScripts"
private val allowedDomains: Set<String> = setOf("*")

private var globalReplyProxy: JavaScriptReplyProxy? = null

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun process(
message: String,
Expand All @@ -72,6 +78,12 @@ class ContentScopeScriptsWebMessagingPlugin @Inject constructor(

jsMessage?.let {
if (context == jsMessage.context) {
// Setup reply proxy so we can send subscription events
if (jsMessage.featureName == "messaging" || jsMessage.method == "initialPing") {
logcat("Cris") { "initialPing" }
globalReplyProxy = replyProxy
}

// Process global handlers first (always processed regardless of feature handlers)
globalHandlers.getPlugins()
.map { it.getGlobalJsMessageHandler() }
Expand Down Expand Up @@ -198,4 +210,29 @@ class ContentScopeScriptsWebMessagingPlugin @Inject constructor(
}
}
}

@SuppressLint("RequiresFeature")
override fun postMessage(subscriptionEventData: SubscriptionEventData) {
runCatching {
appCoroutineScope.launch {
if (!webViewCompatContentScopeScripts.isEnabled()) {
contentScopeScriptsJsMessaging.sendSubscriptionEvent(subscriptionEventData)
return@launch
}

val subscriptionEvent = SubscriptionEvent(
context = context,
featureName = subscriptionEventData.featureName,
subscriptionName = subscriptionEventData.subscriptionName,
params = subscriptionEventData.params,
).let {
moshi.adapter(SubscriptionEvent::class.java).toJson(it)
}

withContext(dispatcherProvider.main()) {
globalReplyProxy?.postMessage(subscriptionEvent)
}
}
}
}
}
Loading
Loading