Skip to content

Commit 10c31c4

Browse files
committed
Support subscriptions by replying to ping message
1 parent 2463927 commit 10c31c4

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/WebViewCompatWebCompatMessagingPlugin.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@ import com.duckduckgo.contentscopescripts.impl.WebViewCompatContentScopeScripts
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.duckduckgo.js.messaging.api.WebViewCompatMessageHandler.ProcessResult.SendResponse
3436
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler.ProcessResult.SendToConsumer
3537
import com.squareup.anvil.annotations.ContributesMultibinding
3638
import com.squareup.moshi.Moshi
3739
import javax.inject.Inject
40+
import kotlinx.coroutines.runBlocking
3841
import kotlinx.coroutines.withContext
3942
import logcat.LogPriority.ERROR
4043
import logcat.asLog
@@ -56,6 +59,8 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
5659
private val context: String = "contentScopeScripts"
5760
private val allowedDomains: Set<String> = setOf("*")
5861

62+
private var globalReplyProxy: JavaScriptReplyProxy? = null
63+
5964
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
6065
internal fun process(
6166
message: String,
@@ -68,6 +73,12 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
6873

6974
jsMessage?.let {
7075
if (context == jsMessage.context) {
76+
// Setup reply proxy so we can send subscription events
77+
if (jsMessage.featureName == "messaging" || jsMessage.method == "initialPing") {
78+
logcat("Cris") { "initialPing" }
79+
globalReplyProxy = replyProxy
80+
}
81+
7182
// Process global handlers first (always processed regardless of feature handlers)
7283
globalHandlers.getPlugins()
7384
.map { it.getGlobalJsMessageHandler() }
@@ -188,4 +199,30 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
188199
replyProxy.postMessage(responseWithId.toString())
189200
}
190201
}
202+
203+
@SuppressLint("RequiresFeature")
204+
override fun postMessage(subscriptionEventData: SubscriptionEventData): Boolean {
205+
return runCatching {
206+
// TODO (cbarreiro) temporary, remove
207+
val newWebCompatApisEnabled = runBlocking {
208+
adsJsContentScopeScripts.isEnabled()
209+
}
210+
211+
if (!newWebCompatApisEnabled) {
212+
return false
213+
}
214+
215+
val subscriptionEvent = SubscriptionEvent(
216+
context = context,
217+
featureName = subscriptionEventData.featureName,
218+
subscriptionName = subscriptionEventData.subscriptionName,
219+
params = subscriptionEventData.params,
220+
).let {
221+
moshi.adapter(SubscriptionEvent::class.java).toJson(it)
222+
}
223+
224+
globalReplyProxy?.postMessage(subscriptionEvent)
225+
true
226+
}.getOrElse { false }
227+
}
191228
}

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,63 @@ class WebViewCompatWebCompatMessagingPluginTest {
239239
assertEquals("contentScopeAdsjs", capturedObjectName)
240240
}
241241

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

js-messaging/js-messaging-api/src/main/java/com/duckduckgo/js/messaging/api/WebViewCompatMessaging.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)