Skip to content

Commit aa744cb

Browse files
committed
Support subscriptions by replying to ping message
1 parent 66684c8 commit aa744cb

File tree

10 files changed

+234
-3
lines changed

10 files changed

+234
-3
lines changed

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,24 +1208,33 @@ class BrowserTabFragment :
12081208
private fun onOmnibarCustomTabPrivacyDashboardPressed() {
12091209
val params = PrivacyDashboardPrimaryScreen(tabId)
12101210
val intent = globalActivityStarter.startIntent(requireContext(), params)
1211-
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
1211+
postBreakageReportingEvent()
12121212
intent?.let { activityResultPrivacyDashboard.launch(intent) }
12131213
pixel.fire(CustomTabPixelNames.CUSTOM_TABS_PRIVACY_DASHBOARD_OPENED)
12141214
}
12151215

1216+
private fun postBreakageReportingEvent() {
1217+
appCoroutineScope.launch {
1218+
val eventData = createBreakageReportingEventData()
1219+
webViewClient.postMessage(eventData) {
1220+
contentScopeScripts.sendSubscriptionEvent(eventData)
1221+
}
1222+
}
1223+
}
1224+
12161225
private fun onFireButtonPressed() {
12171226
val isFocusedNtp = omnibar.viewMode == ViewMode.NewTab && omnibar.getText().isEmpty() && omnibar.omnibarTextInput.hasFocus()
12181227
browserActivity?.launchFire(launchedFromFocusedNtp = isFocusedNtp)
12191228
viewModel.onFireMenuSelected()
12201229
}
12211230

12221231
private fun onBrowserMenuButtonPressed() {
1223-
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
1232+
postBreakageReportingEvent()
12241233
viewModel.onBrowserMenuClicked(isCustomTab = isActiveCustomTab())
12251234
}
12261235

12271236
private fun onOmnibarPrivacyShieldButtonPressed() {
1228-
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
1237+
postBreakageReportingEvent()
12291238
viewModel.onOmnibarPrivacyShieldButtonPressed()
12301239
launchPrivacyDashboard(toggle = false)
12311240
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3672,6 +3672,19 @@ class BrowserTabViewModel @Inject constructor(
36723672
"addDebugFlag" -> {
36733673
site?.debugFlags = (site?.debugFlags ?: listOf()).toMutableList().plus(featureName)?.toList()
36743674
}
3675+
"breakageReportResult" -> if (data != null) {
3676+
breakageReportResult(data)
3677+
}
3678+
"initialPing" -> {
3679+
// TODO: Eventually, we might want plugins here
3680+
val response = JSONObject(
3681+
mapOf(
3682+
"desktopModeEnabled" to (getSite()?.isDesktopMode ?: false),
3683+
"forcedZoomEnabled" to (accessibilityViewState.value?.forceZoom ?: false),
3684+
),
3685+
)
3686+
onResponse(response)
3687+
}
36753688
}
36763689
}
36773690

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
8181
import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On
8282
import com.duckduckgo.duckplayer.impl.DUCK_PLAYER_OPEN_IN_YOUTUBE_PATH
8383
import com.duckduckgo.history.api.NavigationHistory
84+
import com.duckduckgo.js.messaging.api.SubscriptionEventData
8485
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
8586
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
8687
import com.duckduckgo.privacy.config.api.AmpLinks
@@ -781,6 +782,17 @@ class BrowserWebViewClient @Inject constructor(
781782
}
782783
}
783784
}
785+
786+
fun postMessage(
787+
eventData: SubscriptionEventData,
788+
fallback: () -> Unit,
789+
) {
790+
webMessagingPlugins.getPlugins().forEach {
791+
if (!it.postMessage(eventData)) {
792+
fallback()
793+
}
794+
}
795+
}
784796
}
785797

786798
enum class WebViewPixelName(override val pixelName: String) : Pixel.PixelName {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2024 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.breakagereporting.impl
18+
19+
import com.duckduckgo.contentscopescripts.api.WebCompatContentScopeJsMessageHandlersPlugin
20+
import com.duckduckgo.di.scopes.ActivityScope
21+
import com.duckduckgo.js.messaging.api.JsMessage
22+
import com.duckduckgo.js.messaging.api.WebCompatMessageHandler
23+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
24+
import com.squareup.anvil.annotations.ContributesMultibinding
25+
import javax.inject.Inject
26+
import org.json.JSONObject
27+
28+
@ContributesMultibinding(ActivityScope::class)
29+
class WebViewCompatBreakageContentScopeJsMessageHandler @Inject constructor() : WebCompatContentScopeJsMessageHandlersPlugin {
30+
31+
override fun getJsMessageHandler(): WebCompatMessageHandler = object : WebCompatMessageHandler {
32+
33+
override fun process(
34+
jsMessage: JsMessage,
35+
jsMessageCallback: WebViewCompatMessageCallback?,
36+
onResponse: (JSONObject) -> Unit,
37+
) {
38+
jsMessageCallback?.process(featureName, jsMessage.method, jsMessage.id, jsMessage.params, onResponse)
39+
}
40+
41+
override val featureName: String = "breakageReporting"
42+
override val methods: List<String> = listOf("breakageReportResult")
43+
}
44+
}

browser-api/src/main/java/com/duckduckgo/app/browser/api/DuckDuckGoWebView.kt

Whitespace-only changes.

content-scope-scripts/content-scope-scripts-api/src/main/java/com/duckduckgo/contentscopescripts/api/WebMessagingPlugin.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.duckduckgo.contentscopescripts.api
1818

1919
import androidx.webkit.WebViewCompat.WebMessageListener
20+
import com.duckduckgo.js.messaging.api.SubscriptionEventData
2021
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
2122

2223
interface WebMessagingPlugin {
@@ -28,4 +29,6 @@ interface WebMessagingPlugin {
2829
suspend fun unregister(
2930
unregisterer: suspend (objectName: String) -> Boolean,
3031
)
32+
33+
fun postMessage(subscriptionEventData: SubscriptionEventData): Boolean
3134
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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.contentscopescripts.impl
18+
19+
import com.duckduckgo.contentscopescripts.api.WebCompatContentScopeJsMessageHandlersPlugin
20+
import com.duckduckgo.di.scopes.ActivityScope
21+
import com.duckduckgo.js.messaging.api.JsMessage
22+
import com.duckduckgo.js.messaging.api.WebCompatMessageHandler
23+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
24+
import com.squareup.anvil.annotations.ContributesMultibinding
25+
import javax.inject.Inject
26+
import org.json.JSONObject
27+
28+
@ContributesMultibinding(ActivityScope::class)
29+
class WebViewCompatMessagingContentScopeJsMessageHandler @Inject constructor() : WebCompatContentScopeJsMessageHandlersPlugin {
30+
31+
override fun getJsMessageHandler(): WebCompatMessageHandler = object : WebCompatMessageHandler {
32+
33+
override fun process(
34+
jsMessage: JsMessage,
35+
jsMessageCallback: WebViewCompatMessageCallback?,
36+
onResponse: (JSONObject) -> Unit,
37+
) {
38+
jsMessageCallback?.process(featureName, jsMessage.method, jsMessage.id, jsMessage.params, onResponse)
39+
}
40+
41+
override val featureName: String = "messaging"
42+
override val methods: List<String> = listOf("initialPing")
43+
}
44+
}

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ import com.duckduckgo.contentscopescripts.impl.AdsJsContentScopeScripts
2929
import com.duckduckgo.di.scopes.ActivityScope
3030
import com.duckduckgo.js.messaging.api.JsCallbackData
3131
import com.duckduckgo.js.messaging.api.JsMessage
32+
import com.duckduckgo.js.messaging.api.SubscriptionEvent
33+
import com.duckduckgo.js.messaging.api.SubscriptionEventData
3234
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
3335
import com.squareup.anvil.annotations.ContributesMultibinding
3436
import com.squareup.moshi.Moshi
3537
import javax.inject.Inject
38+
import kotlinx.coroutines.runBlocking
3639
import kotlinx.coroutines.withContext
3740
import logcat.LogPriority.ERROR
3841
import logcat.asLog
@@ -54,6 +57,8 @@ class WebCompatMessagingPlugin @Inject constructor(
5457
private val context: String = "contentScopeScripts"
5558
private val allowedDomains: Set<String> = setOf("*")
5659

60+
private var globalReplyProxy: JavaScriptReplyProxy? = null
61+
5762
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
5863
internal fun process(
5964
message: String,
@@ -66,6 +71,12 @@ class WebCompatMessagingPlugin @Inject constructor(
6671

6772
jsMessage?.let {
6873
if (context == jsMessage.context) {
74+
// Setup reply proxy so we can send subscription events
75+
if (jsMessage.featureName == "messaging" || jsMessage.method == "initialPing") {
76+
logcat("Cris") { "initialPing" }
77+
globalReplyProxy = replyProxy
78+
}
79+
6980
// Process global handlers first (always processed regardless of feature handlers)
7081
globalHandlers.getPlugins()
7182
.map { it.getGlobalJsMessageHandler() }
@@ -141,4 +152,30 @@ class WebCompatMessagingPlugin @Inject constructor(
141152
replyProxy.postMessage(responseWithId.toString())
142153
}
143154
}
155+
156+
@SuppressLint("RequiresFeature")
157+
override fun postMessage(subscriptionEventData: SubscriptionEventData): Boolean {
158+
return runCatching {
159+
// TODO (cbarreiro) temporary, remove
160+
val newWebCompatApisEnabled = runBlocking {
161+
adsJsContentScopeScripts.isEnabled()
162+
}
163+
164+
if (!newWebCompatApisEnabled) {
165+
return false
166+
}
167+
168+
val subscriptionEvent = SubscriptionEvent(
169+
context = context,
170+
featureName = subscriptionEventData.featureName,
171+
subscriptionName = subscriptionEventData.subscriptionName,
172+
params = subscriptionEventData.params,
173+
).let {
174+
moshi.adapter(SubscriptionEvent::class.java).toJson(it)
175+
}
176+
177+
globalReplyProxy?.postMessage(subscriptionEvent)
178+
true
179+
}.getOrElse { false }
180+
}
144181
}

content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/messaging/WebCompatMessagingPluginTest.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,63 @@ class WebCompatMessagingPluginTest {
247247
assertEquals("contentScopeAdsjs", capturedObjectName)
248248
}
249249

250+
// @Test
251+
// fun `when posting message and adsjs is disabled then do not post message`() = runTest {
252+
// whenever(adsJsContentScopeScripts.isEnabled()).thenReturn(false)
253+
// val eventData = SubscriptionEventData("feature", "subscription", JSONObject())
254+
// givenInterfaceIsRegistered()
255+
// processInitialPing()
256+
//
257+
// val result = testee.postMessage(eventData)
258+
//
259+
// verify(mockWebView, never()).safePostMessage(any(), any())
260+
// assertFalse(result)
261+
// }
262+
//
263+
// @Test
264+
// fun `when posting message and adsjs is enabled but webView not registered then do not post message`() = runTest {
265+
// whenever(adsJsContentScopeScripts.isEnabled()).thenReturn(true)
266+
// val eventData = SubscriptionEventData("feature", "subscription", JSONObject())
267+
// processInitialPing()
268+
//
269+
// val result = testee.postMessage(eventData)
270+
//
271+
// verify(mockWebView, never()).safePostMessage(any(), any())
272+
// assertFalse(result)
273+
// }
274+
//
275+
// @Test
276+
// fun `when posting message and adsjs is enabled but initialPing not processes then do not post message`() = runTest {
277+
// whenever(adsJsContentScopeScripts.isEnabled()).thenReturn(true)
278+
// val eventData = SubscriptionEventData("feature", "subscription", JSONObject())
279+
//
280+
// val result = testee.postMessage(eventData)
281+
//
282+
// verify(mockWebView, never()).safePostMessage(any(), any())
283+
// assertFalse(result)
284+
// }
285+
//
286+
// @Test
287+
// fun `when posting message after getting initialPing and adsjs is enabled then post message`() = runTest {
288+
// whenever(adsJsContentScopeScripts.isEnabled()).thenReturn(true)
289+
// val eventData = SubscriptionEventData("feature", "subscription", JSONObject())
290+
// givenInterfaceIsRegistered()
291+
// processInitialPing()
292+
// verify(mockWebView, never()).postWebMessage(any(), any())
293+
//
294+
// val result = testee.postMessage(eventData)
295+
//
296+
// verify(mockWebView).safePostMessage(any(), any())
297+
// assertTrue(result)
298+
// }
299+
300+
private fun processInitialPing() = runTest {
301+
val message = """
302+
{"context":"contentScopeScripts","featureName":"messaging","id":"debugId","method":"initialPing","params":{}}
303+
""".trimIndent()
304+
testee.process(message, callback, mockReplyProxy)
305+
}
306+
250307
private val callback = object : WebViewCompatMessageCallback {
251308
var counter = 0
252309
override fun process(

js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/AdsjsMessaging.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ package com.duckduckgo.js.messaging.api
1919
import org.json.JSONObject
2020

2121
interface WebViewCompatMessageCallback {
22+
/**
23+
* Processes a JavaScript message received by the WebView.
24+
*
25+
* This method is responsible for handling a message with the given parameters
26+
* and invoking a callback to send a response back to the JavaScript code.
27+
*
28+
* @param featureName The name of the feature associated with the message.
29+
* @param method The method name of the message.
30+
* @param id An optional identifier for the message.
31+
* @param data An optional JSON object containing additional data for the message.
32+
* @param onResponse A callback function to send a response back to the JavaScript code.
33+
*/
2234
fun process(
2335
featureName: String,
2436
method: String,

0 commit comments

Comments
 (0)