Skip to content

Commit 5bcf4a2

Browse files
committed
Support subscriptions by replying to ping message
1 parent f74512b commit 5bcf4a2

File tree

11 files changed

+237
-3
lines changed

11 files changed

+237
-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
@@ -88,6 +88,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On
8888
import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.Unavailable
8989
import com.duckduckgo.feature.toggles.api.Toggle
9090
import com.duckduckgo.history.api.NavigationHistory
91+
import com.duckduckgo.js.messaging.api.SubscriptionEventData
9192
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
9293
import com.duckduckgo.privacy.config.api.AmpLinks
9394
import com.duckduckgo.subscriptions.api.Subscriptions
@@ -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
@@ -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
@@ -230,6 +230,63 @@ class WebViewCompatWebCompatMessagingPluginTest {
230230
assertEquals("contentScopeAdsjs", capturedObjectName)
231231
}
232232

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

0 commit comments

Comments
 (0)