Skip to content

Commit 55daac3

Browse files
committed
Support message responses
1 parent 67669ee commit 55daac3

File tree

13 files changed

+268
-29
lines changed

13 files changed

+268
-29
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
@@ -301,6 +302,7 @@ import com.duckduckgo.js.messaging.api.JsCallbackData
301302
import com.duckduckgo.js.messaging.api.JsMessageCallback
302303
import com.duckduckgo.js.messaging.api.JsMessaging
303304
import com.duckduckgo.js.messaging.api.SubscriptionEventData
305+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
304306
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
305307
import com.duckduckgo.mobile.android.R as CommonR
306308
import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams
@@ -886,6 +888,13 @@ class BrowserTabFragment :
886888
contentScopeScripts.onResponse(it)
887889
}
888890

891+
private var currentWebShareReplyCallback: ((JSONObject) -> Unit)? = null
892+
893+
private val webViewCompatWebShareLauncher = registerForActivityResult(WebViewCompatWebShareChooser()) { result ->
894+
currentWebShareReplyCallback?.invoke(result)
895+
currentWebShareReplyCallback = null
896+
}
897+
889898
// Instantiating a private class that contains an implementation detail of BrowserTabFragment but is separated for tidiness
890899
// see discussion in https://github.com/duckduckgo/Android/pull/4027#discussion_r1433373625
891900
private val jsOrientationHandler = JsOrientationHandler()
@@ -2148,6 +2157,10 @@ class BrowserTabFragment :
21482157
is Command.SendResponseToJs -> contentScopeScripts.onResponse(it.data)
21492158
is Command.SendResponseToDuckPlayer -> duckPlayerScripts.onResponse(it.data)
21502159
is Command.WebShareRequest -> webShareRequest.launch(it.data)
2160+
is Command.WebViewCompatWebShareRequest -> {
2161+
currentWebShareReplyCallback = it.onResponse
2162+
webViewCompatWebShareLauncher.launch(it.data)
2163+
}
21512164
is Command.ScreenLock -> screenLock(it.data)
21522165
is Command.ScreenUnlock -> screenUnlock()
21532166
is Command.ShowFaviconsPrompt -> showFaviconsPrompt()
@@ -3028,16 +3041,15 @@ class BrowserTabFragment :
30283041
lifecycleScope.launch(dispatchers.main()) {
30293042
webViewClient.configureWebView(
30303043
it,
3031-
object : JsMessageCallback() {
3044+
object : WebViewCompatMessageCallback {
30323045
override fun process(
30333046
featureName: String,
30343047
method: String,
30353048
id: String?,
30363049
data: JSONObject?,
3050+
onResponse: (JSONObject) -> Unit,
30373051
) {
3038-
viewModel.processJsCallbackMessage(featureName, method, id, data, isActiveCustomTab()) {
3039-
it.url
3040-
}
3052+
viewModel.webViewCompatProcessJsCallbackMessage(featureName, method, id, data, onResponse)
30413053
}
30423054
},
30433055
)

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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
7979
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
82+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
8283
import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin
8384
import com.duckduckgo.js.messaging.api.JsMessageCallback
8485
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
@@ -468,7 +469,7 @@ class BrowserWebViewClient @Inject constructor(
468469
webView.settings.mediaPlaybackRequiresUserGesture = mediaPlayback.doesMediaPlaybackRequireUserGestureForUrl(url)
469470
}
470471

471-
fun configureWebView(webView: DuckDuckGoWebView, callback: JsMessageCallback) {
472+
fun configureWebView(webView: DuckDuckGoWebView, callback: WebViewCompatMessageCallback) {
472473
appCoroutineScope.launch {
473474
val activeExperiments = contentScopeExperiments.getActiveExperiments()
474475
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-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: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@
1616

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

19+
import android.annotation.SuppressLint
1920
import android.webkit.WebView
2021
import androidx.annotation.VisibleForTesting
22+
import androidx.webkit.JavaScriptReplyProxy
23+
import androidx.webkit.WebViewCompat.WebMessageListener
2124
import com.duckduckgo.app.di.AppCoroutineScope
2225
import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper
2326
import com.duckduckgo.common.utils.plugins.PluginPoint
2427
import com.duckduckgo.contentscopescripts.api.WebViewCompatContentScopeJsMessageHandlersPlugin
2528
import com.duckduckgo.contentscopescripts.impl.WebViewCompatContentScopeScripts
2629
import com.duckduckgo.di.scopes.ActivityScope
30+
import com.duckduckgo.js.messaging.api.JsCallbackData
2731
import com.duckduckgo.js.messaging.api.JsMessage
28-
import com.duckduckgo.js.messaging.api.JsMessageCallback
2932
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
33+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
3034
import com.squareup.anvil.annotations.ContributesMultibinding
3135
import com.squareup.moshi.Moshi
3236
import javax.inject.Inject
@@ -35,6 +39,7 @@ import kotlinx.coroutines.launch
3539
import logcat.LogPriority.ERROR
3640
import logcat.asLog
3741
import logcat.logcat
42+
import org.json.JSONObject
3843

3944
private const val JS_OBJECT_NAME = "contentScopeAdsjs"
4045

@@ -55,7 +60,8 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
5560
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
5661
internal fun process(
5762
message: String,
58-
jsMessageCallback: JsMessageCallback,
63+
jsMessageCallback: WebViewCompatMessageCallback,
64+
replyProxy: JavaScriptReplyProxy,
5965
) {
6066
try {
6167
val adapter = moshi.adapter(JsMessage::class.java)
@@ -68,13 +74,21 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
6874
.map { it.getGlobalJsMessageHandler() }
6975
.filter { it.method == jsMessage.method }
7076
.forEach { handler ->
71-
handler.process(jsMessage, jsMessageCallback)
77+
handler.process(jsMessage, jsMessageCallback) { }
7278
}
7379

7480
// Process with feature handlers
7581
handlers.getPlugins().map { it.getJsMessageHandler() }.firstOrNull {
7682
it.methods.contains(jsMessage.method) && it.featureName == jsMessage.featureName
77-
}?.process(jsMessage, jsMessageCallback)
83+
}?.process(jsMessage, jsMessageCallback) { response: JSONObject ->
84+
val callbackData = JsCallbackData(
85+
id = jsMessage.id ?: "",
86+
params = response,
87+
featureName = jsMessage.featureName,
88+
method = jsMessage.method,
89+
)
90+
onResponse(callbackData, replyProxy)
91+
}
7892
}
7993
}
8094
} catch (e: Exception) {
@@ -83,7 +97,7 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
8397
}
8498

8599
override fun register(
86-
jsMessageCallback: JsMessageCallback?,
100+
jsMessageCallback: WebViewCompatMessageCallback?,
87101
webView: WebView,
88102
) {
89103
coroutineScope.launch {
@@ -95,10 +109,11 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
95109
webView,
96110
JS_OBJECT_NAME,
97111
allowedDomains,
98-
) { _, message, _, _, _ ->
112+
) { _, message, _, _, replyProxy ->
99113
process(
100114
message.data ?: "",
101115
jsMessageCallback,
116+
replyProxy
102117
)
103118
}
104119
}.getOrElse { exception ->
@@ -121,4 +136,17 @@ class WebViewCompatWebCompatMessagingPlugin @Inject constructor(
121136
}
122137
}
123138
}
139+
140+
@SuppressLint("RequiresFeature")
141+
private fun onResponse(response: JsCallbackData, replyProxy: JavaScriptReplyProxy) {
142+
runCatching {
143+
val responseWithId = JSONObject().apply {
144+
put("id", response.id)
145+
put("result", response.params)
146+
put("featureName", response.featureName)
147+
put("context", context)
148+
}
149+
replyProxy.postMessage(responseWithId.toString())
150+
}
151+
}
124152
}

0 commit comments

Comments
 (0)