Skip to content

Commit b9ed3dd

Browse files
committed
Support subscriptions by replying to ping message
1 parent 983a564 commit b9ed3dd

File tree

11 files changed

+236
-3
lines changed

11 files changed

+236
-3
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.Unavailab
8585
import com.duckduckgo.feature.toggles.api.Toggle
8686
import com.duckduckgo.history.api.NavigationHistory
8787
import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin
88+
import com.duckduckgo.js.messaging.api.SubscriptionEventData
8889
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
8990
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
9091
import com.duckduckgo.privacy.config.api.AmpLinks
@@ -1348,6 +1349,11 @@ class BrowserWebViewClientTest {
13481349
) {
13491350
registered = true
13501351
}
1352+
1353+
// TODO (cbarreiro) Test message posting
1354+
override fun postMessage(subscriptionEventData: SubscriptionEventData): Boolean {
1355+
TODO("Not yet implemented")
1356+
}
13511357
}
13521358

13531359
class FakeWebMessagingPluginPoint : PluginPoint<WebMessagingPlugin> {

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,24 +1194,33 @@ class BrowserTabFragment :
11941194
private fun onOmnibarCustomTabPrivacyDashboardPressed() {
11951195
val params = PrivacyDashboardPrimaryScreen(tabId)
11961196
val intent = globalActivityStarter.startIntent(requireContext(), params)
1197-
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
1197+
postBreakageReportingEvent()
11981198
intent?.let { activityResultPrivacyDashboard.launch(intent) }
11991199
pixel.fire(CustomTabPixelNames.CUSTOM_TABS_PRIVACY_DASHBOARD_OPENED)
12001200
}
12011201

1202+
private fun postBreakageReportingEvent() {
1203+
appCoroutineScope.launch {
1204+
val eventData = createBreakageReportingEventData()
1205+
webViewClient.postMessage(eventData) {
1206+
contentScopeScripts.sendSubscriptionEvent(eventData)
1207+
}
1208+
}
1209+
}
1210+
12021211
private fun onFireButtonPressed() {
12031212
val isFocusedNtp = omnibar.viewMode == ViewMode.NewTab && omnibar.getText().isEmpty() && omnibar.omnibarTextInput.hasFocus()
12041213
browserActivity?.launchFire(launchedFromFocusedNtp = isFocusedNtp)
12051214
viewModel.onFireMenuSelected()
12061215
}
12071216

12081217
private fun onBrowserMenuButtonPressed() {
1209-
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
1218+
postBreakageReportingEvent()
12101219
viewModel.onBrowserMenuClicked(isCustomTab = isActiveCustomTab())
12111220
}
12121221

12131222
private fun onOmnibarPrivacyShieldButtonPressed() {
1214-
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
1223+
postBreakageReportingEvent()
12151224
viewModel.onOmnibarPrivacyShieldButtonPressed()
12161225
launchPrivacyDashboard(toggle = false)
12171226
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3681,6 +3681,19 @@ class BrowserTabViewModel @Inject constructor(
36813681
"addDebugFlag" -> {
36823682
site?.debugFlags = (site?.debugFlags ?: listOf()).toMutableList().plus(featureName)?.toList()
36833683
}
3684+
"breakageReportResult" -> if (data != null) {
3685+
breakageReportResult(data)
3686+
}
3687+
"initialPing" -> {
3688+
// TODO: Eventually, we might want plugins here
3689+
val response = JSONObject(
3690+
mapOf(
3691+
"desktopModeEnabled" to (getSite()?.isDesktopMode ?: false),
3692+
"forcedZoomEnabled" to (accessibilityViewState.value?.forceZoom ?: false),
3693+
),
3694+
)
3695+
onResponse(response)
3696+
}
36843697
}
36853698
}
36863699

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On
8080
import com.duckduckgo.duckplayer.impl.DUCK_PLAYER_OPEN_IN_YOUTUBE_PATH
8181
import com.duckduckgo.history.api.NavigationHistory
8282
import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin
83+
import com.duckduckgo.js.messaging.api.SubscriptionEventData
8384
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
8485
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
8586
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
@@ -770,6 +771,17 @@ class BrowserWebViewClient @Inject constructor(
770771
plugin.unregister(webView)
771772
}
772773
}
774+
775+
fun postMessage(
776+
eventData: SubscriptionEventData,
777+
fallback: () -> Unit,
778+
) {
779+
webMessagingPlugins.getPlugins().forEach {
780+
if (!it.postMessage(eventData)) {
781+
fallback()
782+
}
783+
}
784+
}
773785
}
774786

775787
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.ProcessResult
23+
import com.duckduckgo.js.messaging.api.ProcessResult.SendToConsumer
24+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler
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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.ProcessResult
23+
import com.duckduckgo.js.messaging.api.ProcessResult.SendToConsumer
24+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageHandler
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+
): ProcessResult {
36+
return SendToConsumer
37+
}
38+
39+
override val featureName: String = "messaging"
40+
override val methods: List<String> = listOf("initialPing")
41+
}
42+
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,17 @@ import com.duckduckgo.js.messaging.api.JsCallbackData
3131
import com.duckduckgo.js.messaging.api.JsMessage
3232
import com.duckduckgo.js.messaging.api.ProcessResult.SendResponse
3333
import com.duckduckgo.js.messaging.api.ProcessResult.SendToConsumer
34+
import com.duckduckgo.js.messaging.api.SubscriptionEvent
35+
import com.duckduckgo.js.messaging.api.SubscriptionEventData
3436
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
3537
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
3638
import com.squareup.anvil.annotations.ContributesMultibinding
3739
import com.squareup.moshi.Moshi
3840
import javax.inject.Inject
3941
import kotlinx.coroutines.CoroutineScope
4042
import kotlinx.coroutines.launch
43+
import kotlinx.coroutines.runBlocking
44+
import kotlinx.coroutines.withContext
4145
import logcat.LogPriority.ERROR
4246
import logcat.asLog
4347
import logcat.logcat
@@ -60,6 +64,8 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
6064
private val context: String = "contentScopeScripts"
6165
private val allowedDomains: Set<String> = setOf("*")
6266

67+
private var globalReplyProxy: JavaScriptReplyProxy? = null
68+
6369
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
6470
internal fun process(
6571
message: String,
@@ -72,6 +78,12 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
7278

7379
jsMessage?.let {
7480
if (context == jsMessage.context) {
81+
// Setup reply proxy so we can send subscription events
82+
if (jsMessage.featureName == "messaging" || jsMessage.method == "initialPing") {
83+
logcat("Cris") { "initialPing" }
84+
globalReplyProxy = replyProxy
85+
}
86+
7587
// Process global handlers first (always processed regardless of feature handlers)
7688
globalHandlers.getPlugins()
7789
.map { it.getGlobalJsMessageHandler() }
@@ -199,4 +211,30 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
199211
}
200212
}
201213
}
214+
215+
@SuppressLint("RequiresFeature")
216+
override fun postMessage(subscriptionEventData: SubscriptionEventData): Boolean {
217+
return runCatching {
218+
// TODO (cbarreiro) temporary, remove
219+
val newWebCompatApisEnabled = runBlocking {
220+
webViewCompatContentScopeScripts.isEnabled()
221+
}
222+
223+
if (!newWebCompatApisEnabled) {
224+
return false
225+
}
226+
227+
val subscriptionEvent = SubscriptionEvent(
228+
context = context,
229+
featureName = subscriptionEventData.featureName,
230+
subscriptionName = subscriptionEventData.subscriptionName,
231+
params = subscriptionEventData.params,
232+
).let {
233+
moshi.adapter(SubscriptionEvent::class.java).toJson(it)
234+
}
235+
236+
globalReplyProxy?.postMessage(subscriptionEvent)
237+
true
238+
}.getOrElse { false }
239+
}
202240
}

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
@@ -217,6 +217,63 @@ class WebViewCompatWebCompatMessagingPluginTest {
217217
verify(mockWebViewCompatWrapper).removeWebMessageListener(mockWebView, "contentScopeAdsjs")
218218
}
219219

220+
// @Test
221+
// fun `when posting message and adsjs is disabled then do not post message`() = runTest {
222+
// whenever(adsJsContentScopeScripts.isEnabled()).thenReturn(false)
223+
// val eventData = SubscriptionEventData("feature", "subscription", JSONObject())
224+
// givenInterfaceIsRegistered()
225+
// processInitialPing()
226+
//
227+
// val result = testee.postMessage(eventData)
228+
//
229+
// verify(mockWebView, never()).safePostMessage(any(), any())
230+
// assertFalse(result)
231+
// }
232+
//
233+
// @Test
234+
// fun `when posting message and adsjs is enabled but webView not registered then do not post message`() = runTest {
235+
// whenever(adsJsContentScopeScripts.isEnabled()).thenReturn(true)
236+
// val eventData = SubscriptionEventData("feature", "subscription", JSONObject())
237+
// processInitialPing()
238+
//
239+
// val result = testee.postMessage(eventData)
240+
//
241+
// verify(mockWebView, never()).safePostMessage(any(), any())
242+
// assertFalse(result)
243+
// }
244+
//
245+
// @Test
246+
// fun `when posting message and adsjs is enabled but initialPing not processes then do not post message`() = runTest {
247+
// whenever(adsJsContentScopeScripts.isEnabled()).thenReturn(true)
248+
// val eventData = SubscriptionEventData("feature", "subscription", JSONObject())
249+
//
250+
// val result = testee.postMessage(eventData)
251+
//
252+
// verify(mockWebView, never()).safePostMessage(any(), any())
253+
// assertFalse(result)
254+
// }
255+
//
256+
// @Test
257+
// fun `when posting message after getting initialPing and adsjs is enabled then post message`() = runTest {
258+
// whenever(adsJsContentScopeScripts.isEnabled()).thenReturn(true)
259+
// val eventData = SubscriptionEventData("feature", "subscription", JSONObject())
260+
// givenInterfaceIsRegistered()
261+
// processInitialPing()
262+
// verify(mockWebView, never()).postWebMessage(any(), any())
263+
//
264+
// val result = testee.postMessage(eventData)
265+
//
266+
// verify(mockWebView).safePostMessage(any(), any())
267+
// assertTrue(result)
268+
// }
269+
270+
private fun processInitialPing() = runTest {
271+
val message = """
272+
{"context":"contentScopeScripts","featureName":"messaging","id":"debugId","method":"initialPing","params":{}}
273+
""".trimIndent()
274+
testee.process(message, callback, mockReplyProxy)
275+
}
276+
220277
private val callback = object : WebViewCompatMessageCallback {
221278
var counter = 0
222279
override fun process(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@ interface WebMessagingPlugin {
2727
fun unregister(
2828
webView: WebView,
2929
)
30+
31+
fun postMessage(subscriptionEventData: SubscriptionEventData): Boolean
3032
}

0 commit comments

Comments
 (0)