Skip to content

Commit d5b3be5

Browse files
committed
Support subscriptions by replying to ping message
1 parent 662d037 commit d5b3be5

File tree

10 files changed

+231
-3
lines changed

10 files changed

+231
-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,42 @@
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.WebViewCompatContentScopeJsMessageHandlersPlugin
20+
import com.duckduckgo.di.scopes.ActivityScope
21+
import com.duckduckgo.js.messaging.api.JsMessage
22+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler
23+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler.ProcessResult
24+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler.ProcessResult.SendToConsumer
25+
import com.squareup.anvil.annotations.ContributesMultibinding
26+
import javax.inject.Inject
27+
28+
@ContributesMultibinding(ActivityScope::class)
29+
class WebViewCompatBreakageContentScopeJsMessageHandler @Inject constructor() : WebViewCompatContentScopeJsMessageHandlersPlugin {
30+
31+
override fun getJsMessageHandler(): WebViewCompatMessageHandler = object : WebViewCompatMessageHandler {
32+
33+
override fun process(
34+
jsMessage: JsMessage,
35+
): ProcessResult {
36+
return SendToConsumer
37+
}
38+
39+
override val featureName: String = "breakageReporting"
40+
override val methods: List<String> = listOf("breakageReportResult")
41+
}
42+
}

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,43 @@
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.WebViewCompatContentScopeJsMessageHandlersPlugin
20+
import com.duckduckgo.di.scopes.ActivityScope
21+
import com.duckduckgo.js.messaging.api.JsMessage
22+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler
23+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler.ProcessResult
24+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler.ProcessResult.SendToConsumer
25+
import com.squareup.anvil.annotations.ContributesMultibinding
26+
import javax.inject.Inject
27+
28+
@ContributesMultibinding(ActivityScope::class)
29+
class WebViewCompatMessagingContentScopeJsMessageHandler @Inject constructor() : WebViewCompatContentScopeJsMessageHandlersPlugin {
30+
31+
override fun getJsMessageHandler(): WebViewCompatMessageHandler = object : WebViewCompatMessageHandler {
32+
33+
override fun process(
34+
jsMessage: JsMessage,
35+
36+
): ProcessResult {
37+
return SendToConsumer
38+
}
39+
40+
override val featureName: String = "messaging"
41+
override val methods: List<String> = listOf("initialPing")
42+
}
43+
}

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
@@ -28,12 +28,15 @@ import com.duckduckgo.contentscopescripts.impl.WebViewCompatContentScopeScripts
2828
import com.duckduckgo.di.scopes.ActivityScope
2929
import com.duckduckgo.js.messaging.api.JsCallbackData
3030
import com.duckduckgo.js.messaging.api.JsMessage
31+
import com.duckduckgo.js.messaging.api.SubscriptionEvent
32+
import com.duckduckgo.js.messaging.api.SubscriptionEventData
3133
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
3234
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler.ProcessResult.SendResponse
3335
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler.ProcessResult.SendToConsumer
3436
import com.squareup.anvil.annotations.ContributesMultibinding
3537
import com.squareup.moshi.Moshi
3638
import javax.inject.Inject
39+
import kotlinx.coroutines.runBlocking
3740
import kotlinx.coroutines.withContext
3841
import logcat.LogPriority.ERROR
3942
import logcat.asLog
@@ -55,6 +58,8 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
5558
private val context: String = "contentScopeScripts"
5659
private val allowedDomains: Set<String> = setOf("*")
5760

61+
private var globalReplyProxy: JavaScriptReplyProxy? = null
62+
5863
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
5964
internal fun process(
6065
message: String,
@@ -67,6 +72,12 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
6772

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

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
@@ -237,6 +237,63 @@ class WebViewCompatWebCompatMessagingPluginTest {
237237
assertEquals("contentScopeAdsjs", capturedObjectName)
238238
}
239239

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