Skip to content

Commit 377ea75

Browse files
committed
Support message responses
1 parent 98d5d0b commit 377ea75

File tree

13 files changed

+269
-31
lines changed

13 files changed

+269
-31
lines changed

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ import com.duckduckgo.app.browser.viewstate.PrivacyShieldViewState
178178
import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState
179179
import com.duckduckgo.app.browser.webauthn.WebViewPasskeyInitializer
180180
import com.duckduckgo.app.browser.webshare.WebShareChooser
181+
import com.duckduckgo.app.browser.webshare.WebViewCompatWebShareChooser
181182
import com.duckduckgo.app.browser.webview.WebContentDebugging
182183
import com.duckduckgo.app.browser.webview.WebViewBlobDownloadFeature
183184
import com.duckduckgo.app.browser.webview.safewebview.SafeWebViewFeature
@@ -300,6 +301,7 @@ import com.duckduckgo.js.messaging.api.JsCallbackData
300301
import com.duckduckgo.js.messaging.api.JsMessageCallback
301302
import com.duckduckgo.js.messaging.api.JsMessaging
302303
import com.duckduckgo.js.messaging.api.SubscriptionEventData
304+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
303305
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
304306
import com.duckduckgo.mobile.android.R as CommonR
305307
import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams
@@ -882,6 +884,13 @@ class BrowserTabFragment :
882884
contentScopeScripts.onResponse(it)
883885
}
884886

887+
private var currentWebShareReplyCallback: ((JSONObject) -> Unit)? = null
888+
889+
private val webViewCompatWebShareLauncher = registerForActivityResult(WebViewCompatWebShareChooser()) { result ->
890+
currentWebShareReplyCallback?.invoke(result)
891+
currentWebShareReplyCallback = null
892+
}
893+
885894
// Instantiating a private class that contains an implementation detail of BrowserTabFragment but is separated for tidiness
886895
// see discussion in https://github.com/duckduckgo/Android/pull/4027#discussion_r1433373625
887896
private val jsOrientationHandler = JsOrientationHandler()
@@ -2144,6 +2153,10 @@ class BrowserTabFragment :
21442153
is Command.SendResponseToJs -> contentScopeScripts.onResponse(it.data)
21452154
is Command.SendResponseToDuckPlayer -> duckPlayerScripts.onResponse(it.data)
21462155
is Command.WebShareRequest -> webShareRequest.launch(it.data)
2156+
is Command.WebViewCompatWebShareRequest -> {
2157+
currentWebShareReplyCallback = it.onResponse
2158+
webViewCompatWebShareLauncher.launch(it.data)
2159+
}
21472160
is Command.ScreenLock -> screenLock(it.data)
21482161
is Command.ScreenUnlock -> screenUnlock()
21492162
is Command.ShowFaviconsPrompt -> showFaviconsPrompt()
@@ -3024,16 +3037,15 @@ class BrowserTabFragment :
30243037
lifecycleScope.launch(dispatchers.main()) {
30253038
webViewClient.configureWebView(
30263039
it,
3027-
object : JsMessageCallback() {
3040+
object : WebViewCompatMessageCallback {
30283041
override fun process(
30293042
featureName: String,
30303043
method: String,
30313044
id: String?,
30323045
data: JSONObject?,
3046+
onResponse: (JSONObject) -> Unit,
30333047
) {
3034-
viewModel.processJsCallbackMessage(featureName, method, id, data, isActiveCustomTab()) {
3035-
it.url
3036-
}
3048+
viewModel.webViewCompatProcessJsCallbackMessage(featureName, method, id, data, onResponse)
30373049
}
30383050
},
30393051
)

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ import com.duckduckgo.app.browser.commands.Command.ShowWebPageTitle
162162
import com.duckduckgo.app.browser.commands.Command.StartTrackersExperimentShieldPopAnimation
163163
import com.duckduckgo.app.browser.commands.Command.ToggleReportFeedback
164164
import com.duckduckgo.app.browser.commands.Command.WebShareRequest
165+
import com.duckduckgo.app.browser.commands.Command.WebViewCompatWebShareRequest
165166
import com.duckduckgo.app.browser.commands.Command.WebViewError
166167
import com.duckduckgo.app.browser.commands.NavigationCommand
167168
import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames
@@ -3657,6 +3658,23 @@ class BrowserTabViewModel @Inject constructor(
36573658
)
36583659
}
36593660

3661+
fun webViewCompatProcessJsCallbackMessage(
3662+
featureName: String,
3663+
method: String,
3664+
id: String?,
3665+
data: JSONObject?,
3666+
onResponse: (JSONObject) -> Unit,
3667+
) {
3668+
when (method) {
3669+
"webShare" -> if (id != null && data != null) {
3670+
webViewCompatWebShare(featureName, method, id, data, onResponse)
3671+
}
3672+
"addDebugFlag" -> {
3673+
site?.debugFlags = (site?.debugFlags ?: listOf()).toMutableList().plus(featureName)?.toList()
3674+
}
3675+
}
3676+
}
3677+
36603678
fun processJsCallbackMessage(
36613679
featureName: String,
36623680
method: String,
@@ -3743,6 +3761,18 @@ class BrowserTabViewModel @Inject constructor(
37433761
}
37443762
}
37453763

3764+
private fun webViewCompatWebShare(
3765+
featureName: String,
3766+
method: String,
3767+
id: String,
3768+
data: JSONObject,
3769+
onResponse: (JSONObject) -> Unit,
3770+
) {
3771+
viewModelScope.launch(dispatchers.main()) {
3772+
command.value = WebViewCompatWebShareRequest(JsCallbackData(data, featureName, method, id), onResponse)
3773+
}
3774+
}
3775+
37463776
private fun permissionsQuery(
37473777
featureName: String,
37483778
method: String,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +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.JsMessageCallback
84+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
8585
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
8686
import com.duckduckgo.privacy.config.api.AmpLinks
8787
import com.duckduckgo.subscriptions.api.Subscriptions
@@ -468,7 +468,7 @@ class BrowserWebViewClient @Inject constructor(
468468
webView.settings.mediaPlaybackRequiresUserGesture = mediaPlayback.doesMediaPlaybackRequireUserGestureForUrl(url)
469469
}
470470

471-
fun configureWebView(webView: DuckDuckGoWebView, callback: JsMessageCallback) {
471+
fun configureWebView(webView: DuckDuckGoWebView, callback: WebViewCompatMessageCallback) {
472472
appCoroutineScope.launch {
473473
val activeExperiments = contentScopeExperiments.getActiveExperiments()
474474
addDocumentStartJavascriptPlugins.getPlugins().forEach { plugin ->

app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
4949
import com.duckduckgo.privacy.dashboard.api.ui.DashboardOpener
5050
import com.duckduckgo.savedsites.api.models.SavedSite
5151
import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissions
52+
import org.json.JSONObject
5253

5354
sealed class Command {
5455
class OpenInNewTab(
@@ -249,6 +250,7 @@ sealed class Command {
249250
data class SendResponseToDuckPlayer(val data: JsCallbackData) : Command()
250251
data class SendSubscriptions(val cssData: SubscriptionEventData, val duckPlayerData: SubscriptionEventData) : Command()
251252
data class WebShareRequest(val data: JsCallbackData) : Command()
253+
data class WebViewCompatWebShareRequest(val data: JsCallbackData, val onResponse: (JSONObject) -> Unit) : Command()
252254
data class ScreenLock(val data: JsCallbackData) : Command()
253255
data object ScreenUnlock : Command()
254256
data object ShowFaviconsPrompt : Command()
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) 2023 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.app.browser.webshare
18+
19+
import android.app.Activity
20+
import android.content.Context
21+
import android.content.Intent
22+
import androidx.activity.result.contract.ActivityResultContract
23+
import com.duckduckgo.js.messaging.api.JsCallbackData
24+
import org.json.JSONObject
25+
26+
class WebViewCompatWebShareChooser : ActivityResultContract<JsCallbackData, JSONObject>() {
27+
28+
lateinit var data: JsCallbackData
29+
override fun createIntent(
30+
context: Context,
31+
input: JsCallbackData,
32+
): Intent {
33+
data = input
34+
val url = runCatching { input.params.getString("url") }.getOrNull().orEmpty()
35+
val text = runCatching { input.params.getString("text") }.getOrNull().orEmpty()
36+
val title = runCatching { input.params.getString("title") }.getOrNull().orEmpty()
37+
38+
val finalText = url.ifEmpty { text }
39+
40+
val getContentIntent = Intent(Intent.ACTION_SEND).apply {
41+
type = "text/plain"
42+
putExtra(Intent.EXTRA_TEXT, finalText)
43+
if (title.isNotEmpty()) {
44+
putExtra(Intent.EXTRA_TITLE, title)
45+
}
46+
}
47+
48+
return Intent.createChooser(getContentIntent, title)
49+
}
50+
51+
override fun parseResult(
52+
resultCode: Int,
53+
intent: Intent?,
54+
): JSONObject {
55+
val result = if (this::data.isInitialized) {
56+
when (resultCode) {
57+
Activity.RESULT_OK -> {
58+
JSONObject(EMPTY)
59+
}
60+
Activity.RESULT_CANCELED -> {
61+
JSONObject(ABORT_ERROR)
62+
}
63+
else -> {
64+
JSONObject(DATA_ERROR)
65+
}
66+
}
67+
} else {
68+
JSONObject(DATA_ERROR)
69+
}
70+
return result
71+
}
72+
73+
companion object {
74+
const val EMPTY = """{}"""
75+
const val ABORT_ERROR = """{"failure":{"name":"AbortError","message":"Share canceled"}}"""
76+
const val DATA_ERROR = """{"failure":{"name":"DataError","message":"Data not found"}}"""
77+
}
78+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
package com.duckduckgo.contentscopescripts.api
1818

1919
import com.duckduckgo.js.messaging.api.JsMessage
20-
import com.duckduckgo.js.messaging.api.JsMessageCallback
20+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
21+
import org.json.JSONObject
2122

2223
/**
2324
* Plugin interface for global message handlers that should always be processed
@@ -48,7 +49,8 @@ interface GlobalJsMessageHandler {
4849
*/
4950
fun process(
5051
jsMessage: JsMessage,
51-
jsMessageCallback: JsMessageCallback,
52+
jsMessageCallback: WebViewCompatMessageCallback,
53+
onResponse: (JSONObject) -> Unit,
5254
)
5355

5456
/**

content-scope-scripts/content-scope-scripts-api/src/main/java/com/duckduckgo/contentscopescripts/api/WebMessagingPlugin.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
package com.duckduckgo.contentscopescripts.api
1818

1919
import androidx.webkit.WebViewCompat.WebMessageListener
20-
import com.duckduckgo.js.messaging.api.JsMessageCallback
20+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
2121

2222
interface WebMessagingPlugin {
2323
suspend fun register(
24-
jsMessageCallback: JsMessageCallback?,
24+
jsMessageCallback: WebViewCompatMessageCallback?,
2525
registerer: suspend (objectName: String, allowedOriginRules: Set<String>, webMessageListener: WebMessageListener) -> Boolean,
2626
)
2727

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ import com.duckduckgo.contentscopescripts.api.GlobalContentScopeJsMessageHandler
2020
import com.duckduckgo.contentscopescripts.api.GlobalJsMessageHandler
2121
import com.duckduckgo.di.scopes.AppScope
2222
import com.duckduckgo.js.messaging.api.JsMessage
23-
import com.duckduckgo.js.messaging.api.JsMessageCallback
23+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
2424
import com.squareup.anvil.annotations.ContributesMultibinding
2525
import javax.inject.Inject
2626
import logcat.logcat
27+
import org.json.JSONObject
2728

2829
@ContributesMultibinding(AppScope::class)
2930
class DebugFlagGlobalHandler @Inject constructor() : GlobalContentScopeJsMessageHandlersPlugin {
@@ -32,7 +33,8 @@ class DebugFlagGlobalHandler @Inject constructor() : GlobalContentScopeJsMessage
3233

3334
override fun process(
3435
jsMessage: JsMessage,
35-
jsMessageCallback: JsMessageCallback,
36+
jsMessageCallback: WebViewCompatMessageCallback,
37+
onResponse: (JSONObject) -> Unit,
3638
) {
3739
if (jsMessage.method == method) {
3840
logcat { "DebugFlagGlobalHandler addDebugFlag: ${jsMessage.featureName}" }
@@ -41,6 +43,7 @@ class DebugFlagGlobalHandler @Inject constructor() : GlobalContentScopeJsMessage
4143
method = jsMessage.method,
4244
id = jsMessage.id,
4345
data = jsMessage.params,
46+
onResponse = onResponse,
4447
)
4548
}
4649
}

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

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package com.duckduckgo.contentscopescripts.impl.messaging
1818

19+
import android.annotation.SuppressLint
1920
import androidx.annotation.VisibleForTesting
21+
import androidx.webkit.JavaScriptReplyProxy
2022
import androidx.webkit.WebViewCompat.WebMessageListener
2123
import com.duckduckgo.common.utils.DispatcherProvider
2224
import com.duckduckgo.common.utils.plugins.PluginPoint
@@ -25,15 +27,17 @@ import com.duckduckgo.contentscopescripts.api.WebCompatContentScopeJsMessageHand
2527
import com.duckduckgo.contentscopescripts.api.WebMessagingPlugin
2628
import com.duckduckgo.contentscopescripts.impl.AdsJsContentScopeScripts
2729
import com.duckduckgo.di.scopes.ActivityScope
30+
import com.duckduckgo.js.messaging.api.JsCallbackData
2831
import com.duckduckgo.js.messaging.api.JsMessage
29-
import com.duckduckgo.js.messaging.api.JsMessageCallback
32+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
3033
import com.squareup.anvil.annotations.ContributesMultibinding
3134
import com.squareup.moshi.Moshi
3235
import javax.inject.Inject
3336
import kotlinx.coroutines.withContext
3437
import logcat.LogPriority.ERROR
3538
import logcat.asLog
3639
import logcat.logcat
40+
import org.json.JSONObject
3741

3842
private const val JS_OBJECT_NAME = "contentScopeAdsjs"
3943

@@ -53,7 +57,8 @@ class WebCompatMessagingPlugin @Inject constructor(
5357
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
5458
internal fun process(
5559
message: String,
56-
jsMessageCallback: JsMessageCallback,
60+
jsMessageCallback: WebViewCompatMessageCallback,
61+
replyProxy: JavaScriptReplyProxy,
5762
) {
5863
try {
5964
val adapter = moshi.adapter(JsMessage::class.java)
@@ -66,13 +71,21 @@ class WebCompatMessagingPlugin @Inject constructor(
6671
.map { it.getGlobalJsMessageHandler() }
6772
.filter { it.method == jsMessage.method }
6873
.forEach { handler ->
69-
handler.process(jsMessage, jsMessageCallback)
74+
handler.process(jsMessage, jsMessageCallback) { }
7075
}
7176

7277
// Process with feature handlers
7378
handlers.getPlugins().map { it.getJsMessageHandler() }.firstOrNull {
7479
it.methods.contains(jsMessage.method) && it.featureName == jsMessage.featureName
75-
}?.process(jsMessage, jsMessageCallback)
80+
}?.process(jsMessage, jsMessageCallback) { response: JSONObject ->
81+
val callbackData = JsCallbackData(
82+
id = jsMessage.id ?: "",
83+
params = response,
84+
featureName = jsMessage.featureName,
85+
method = jsMessage.method,
86+
)
87+
onResponse(callbackData, replyProxy)
88+
}
7689
}
7790
}
7891
} catch (e: Exception) {
@@ -81,7 +94,7 @@ class WebCompatMessagingPlugin @Inject constructor(
8194
}
8295

8396
override suspend fun register(
84-
jsMessageCallback: JsMessageCallback?,
97+
jsMessageCallback: WebViewCompatMessageCallback?,
8598
registerer: suspend (objectName: String, allowedOriginRules: Set<String>, webMessageListener: WebMessageListener) -> Boolean,
8699
) {
87100
if (withContext(dispatcherProvider.io()) { !adsJsContentScopeScripts.isEnabled() }) return
@@ -91,7 +104,9 @@ class WebCompatMessagingPlugin @Inject constructor(
91104
return@runCatching registerer(
92105
JS_OBJECT_NAME,
93106
allowedDomains,
94-
WebMessageListener { _, message, _, _, _ -> process(message.data ?: "", jsMessageCallback) },
107+
WebMessageListener { _, message, _, _, replyProxy ->
108+
process(message.data ?: "", jsMessageCallback, replyProxy)
109+
},
95110
)
96111
}.getOrElse { exception ->
97112
logcat(ERROR) { "Error adding WebMessageListener for contentScopeAdsjs: ${exception.asLog()}" }
@@ -113,4 +128,17 @@ class WebCompatMessagingPlugin @Inject constructor(
113128
}
114129
}
115130
}
131+
132+
@SuppressLint("RequiresFeature")
133+
private fun onResponse(response: JsCallbackData, replyProxy: JavaScriptReplyProxy) {
134+
runCatching {
135+
val responseWithId = JSONObject().apply {
136+
put("id", response.id)
137+
put("result", response.params)
138+
put("featureName", response.featureName)
139+
put("context", context)
140+
}
141+
replyProxy.postMessage(responseWithId.toString())
142+
}
143+
}
116144
}

0 commit comments

Comments
 (0)