Skip to content

Commit 16ef071

Browse files
committed
Support subscriptions by replying to ping message
1 parent 4c223f6 commit 16ef071

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
@@ -87,6 +87,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.Unavailab
8787
import com.duckduckgo.feature.toggles.api.Toggle
8888
import com.duckduckgo.history.api.NavigationHistory
8989
import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin
90+
import com.duckduckgo.js.messaging.api.SubscriptionEventData
9091
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
9192
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
9293
import com.duckduckgo.privacy.config.api.AmpLinks
@@ -1350,6 +1351,11 @@ class BrowserWebViewClientTest {
13501351
) {
13511352
registered = true
13521353
}
1354+
1355+
// TODO (cbarreiro) Test message posting
1356+
override fun postMessage(subscriptionEventData: SubscriptionEventData): Boolean {
1357+
TODO("Not yet implemented")
1358+
}
13531359
}
13541360

13551361
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
@@ -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
@@ -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
@@ -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.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
@@ -30,13 +30,17 @@ import com.duckduckgo.js.messaging.api.JsCallbackData
3030
import com.duckduckgo.js.messaging.api.JsMessage
3131
import com.duckduckgo.js.messaging.api.ProcessResult.SendResponse
3232
import com.duckduckgo.js.messaging.api.ProcessResult.SendToConsumer
33+
import com.duckduckgo.js.messaging.api.SubscriptionEvent
34+
import com.duckduckgo.js.messaging.api.SubscriptionEventData
3335
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
3436
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
3537
import com.squareup.anvil.annotations.ContributesMultibinding
3638
import com.squareup.moshi.Moshi
3739
import javax.inject.Inject
3840
import kotlinx.coroutines.CoroutineScope
3941
import kotlinx.coroutines.launch
42+
import kotlinx.coroutines.runBlocking
43+
import kotlinx.coroutines.withContext
4044
import logcat.LogPriority.ERROR
4145
import logcat.asLog
4246
import logcat.logcat
@@ -58,6 +62,8 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
5862
private val context: String = "contentScopeScripts"
5963
private val allowedDomains: Set<String> = setOf("*")
6064

65+
private var globalReplyProxy: JavaScriptReplyProxy? = null
66+
6167
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
6268
internal fun process(
6369
message: String,
@@ -70,6 +76,12 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
7076

7177
jsMessage?.let {
7278
if (context == jsMessage.context) {
79+
// Setup reply proxy so we can send subscription events
80+
if (jsMessage.featureName == "messaging" || jsMessage.method == "initialPing") {
81+
logcat("Cris") { "initialPing" }
82+
globalReplyProxy = replyProxy
83+
}
84+
7385
// Process global handlers first (always processed regardless of feature handlers)
7486
globalHandlers.getPlugins()
7587
.map { it.getGlobalJsMessageHandler() }
@@ -194,4 +206,30 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
194206
}
195207
}
196208
}
209+
210+
@SuppressLint("RequiresFeature")
211+
override fun postMessage(subscriptionEventData: SubscriptionEventData): Boolean {
212+
return runCatching {
213+
// TODO (cbarreiro) temporary, remove
214+
val newWebCompatApisEnabled = runBlocking {
215+
webViewCompatContentScopeScripts.isEnabled()
216+
}
217+
218+
if (!newWebCompatApisEnabled) {
219+
return false
220+
}
221+
222+
val subscriptionEvent = SubscriptionEvent(
223+
context = context,
224+
featureName = subscriptionEventData.featureName,
225+
subscriptionName = subscriptionEventData.subscriptionName,
226+
params = subscriptionEventData.params,
227+
).let {
228+
moshi.adapter(SubscriptionEvent::class.java).toJson(it)
229+
}
230+
231+
globalReplyProxy?.postMessage(subscriptionEvent)
232+
true
233+
}.getOrElse { false }
234+
}
197235
}

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
@@ -234,6 +234,63 @@ class WebViewCompatWebCompatMessagingPluginTest {
234234
assertEquals("contentScopeAdsjs", capturedObjectName)
235235
}
236236

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

0 commit comments

Comments
 (0)