Skip to content

Commit cf7719b

Browse files
committed
Support message responses
1 parent a009c90 commit cf7719b

File tree

13 files changed

+268
-31
lines changed

13 files changed

+268
-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/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
@@ -18,10 +18,11 @@ package com.duckduckgo.contentscopescripts.impl.messaging
1818

1919
import com.duckduckgo.di.scopes.AppScope
2020
import com.duckduckgo.js.messaging.api.JsMessage
21-
import com.duckduckgo.js.messaging.api.JsMessageCallback
21+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
2222
import com.squareup.anvil.annotations.ContributesMultibinding
2323
import javax.inject.Inject
2424
import logcat.logcat
25+
import org.json.JSONObject
2526

2627
@ContributesMultibinding(AppScope::class)
2728
class DebugFlagGlobalHandler @Inject constructor() : GlobalContentScopeJsMessageHandlersPlugin {
@@ -31,7 +32,8 @@ class DebugFlagGlobalHandler @Inject constructor() : GlobalContentScopeJsMessage
3132

3233
override fun process(
3334
jsMessage: JsMessage,
34-
jsMessageCallback: JsMessageCallback,
35+
jsMessageCallback: WebViewCompatMessageCallback,
36+
onResponse: (JSONObject) -> Unit,
3537
) {
3638
if (jsMessage.method == method) {
3739
logcat { "DebugFlagGlobalHandler addDebugFlag: ${jsMessage.featureName}" }
@@ -40,6 +42,7 @@ class DebugFlagGlobalHandler @Inject constructor() : GlobalContentScopeJsMessage
4042
method = jsMessage.method,
4143
id = jsMessage.id,
4244
data = jsMessage.params,
45+
onResponse = onResponse,
4346
)
4447
}
4548
}

content-scope-scripts/content-scope-scripts-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/messaging/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.impl.messaging
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-impl/src/main/java/com/duckduckgo/contentscopescripts/impl/messaging/WebViewCompatWebCompatMessagingPlugin.kt

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,27 @@
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
2325
import com.duckduckgo.contentscopescripts.api.WebMessagingPlugin
2426
import com.duckduckgo.contentscopescripts.api.WebViewCompatContentScopeJsMessageHandlersPlugin
2527
import com.duckduckgo.contentscopescripts.impl.WebViewCompatContentScopeScripts
2628
import com.duckduckgo.di.scopes.ActivityScope
29+
import com.duckduckgo.js.messaging.api.JsCallbackData
2730
import com.duckduckgo.js.messaging.api.JsMessage
28-
import com.duckduckgo.js.messaging.api.JsMessageCallback
31+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
2932
import com.squareup.anvil.annotations.ContributesMultibinding
3033
import com.squareup.moshi.Moshi
3134
import javax.inject.Inject
3235
import kotlinx.coroutines.withContext
3336
import logcat.LogPriority.ERROR
3437
import logcat.asLog
3538
import logcat.logcat
39+
import org.json.JSONObject
3640

3741
private const val JS_OBJECT_NAME = "contentScopeAdsjs"
3842

@@ -52,7 +56,8 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
5256
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
5357
internal fun process(
5458
message: String,
55-
jsMessageCallback: JsMessageCallback,
59+
jsMessageCallback: WebViewCompatMessageCallback,
60+
replyProxy: JavaScriptReplyProxy,
5661
) {
5762
try {
5863
val adapter = moshi.adapter(JsMessage::class.java)
@@ -65,13 +70,21 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
6570
.map { it.getGlobalJsMessageHandler() }
6671
.filter { it.method == jsMessage.method }
6772
.forEach { handler ->
68-
handler.process(jsMessage, jsMessageCallback)
73+
handler.process(jsMessage, jsMessageCallback) { }
6974
}
7075

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

8295
override suspend fun register(
83-
jsMessageCallback: JsMessageCallback?,
96+
jsMessageCallback: WebViewCompatMessageCallback?,
8497
registerer: suspend (objectName: String, allowedOriginRules: Set<String>, webMessageListener: WebMessageListener) -> Boolean,
8598
) {
8699
if (withContext(dispatcherProvider.io()) { !webViewCompatContentScopeScripts.isEnabled() }) return
@@ -90,10 +103,11 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
90103
return@runCatching registerer(
91104
JS_OBJECT_NAME,
92105
allowedDomains,
93-
WebMessageListener { _, message, _, _, _ ->
106+
WebMessageListener { _, message, _, _, replyProxy ->
94107
process(
95108
message.data ?: "",
96109
jsMessageCallback,
110+
replyProxy
97111
)
98112
},
99113
)
@@ -117,4 +131,17 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
117131
}
118132
}
119133
}
134+
135+
@SuppressLint("RequiresFeature")
136+
private fun onResponse(response: JsCallbackData, replyProxy: JavaScriptReplyProxy) {
137+
runCatching {
138+
val responseWithId = JSONObject().apply {
139+
put("id", response.id)
140+
put("result", response.params)
141+
put("featureName", response.featureName)
142+
put("context", context)
143+
}
144+
replyProxy.postMessage(responseWithId.toString())
145+
}
146+
}
120147
}

0 commit comments

Comments
 (0)