Skip to content

Commit 3ad0301

Browse files
committed
Support message responses
1 parent 295589a commit 3ad0301

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
@@ -176,6 +176,7 @@ import com.duckduckgo.app.browser.viewstate.PrivacyShieldViewState
176176
import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState
177177
import com.duckduckgo.app.browser.webauthn.WebViewPasskeyInitializer
178178
import com.duckduckgo.app.browser.webshare.WebShareChooser
179+
import com.duckduckgo.app.browser.webshare.WebViewCompatWebShareChooser
179180
import com.duckduckgo.app.browser.webview.WebContentDebugging
180181
import com.duckduckgo.app.browser.webview.WebViewBlobDownloadFeature
181182
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
@@ -884,6 +886,13 @@ class BrowserTabFragment :
884886
contentScopeScripts.onResponse(it)
885887
}
886888

889+
private var currentWebShareReplyCallback: ((JSONObject) -> Unit)? = null
890+
891+
private val webViewCompatWebShareLauncher = registerForActivityResult(WebViewCompatWebShareChooser()) { result ->
892+
currentWebShareReplyCallback?.invoke(result)
893+
currentWebShareReplyCallback = null
894+
}
895+
887896
// Instantiating a private class that contains an implementation detail of BrowserTabFragment but is separated for tidiness
888897
// see discussion in https://github.com/duckduckgo/Android/pull/4027#discussion_r1433373625
889898
private val jsOrientationHandler = JsOrientationHandler()
@@ -2124,6 +2133,10 @@ class BrowserTabFragment :
21242133
is Command.SendResponseToJs -> contentScopeScripts.onResponse(it.data)
21252134
is Command.SendResponseToDuckPlayer -> duckPlayerScripts.onResponse(it.data)
21262135
is Command.WebShareRequest -> webShareRequest.launch(it.data)
2136+
is Command.WebViewCompatWebShareRequest -> {
2137+
currentWebShareReplyCallback = it.onResponse
2138+
webViewCompatWebShareLauncher.launch(it.data)
2139+
}
21272140
is Command.ScreenLock -> screenLock(it.data)
21282141
is Command.ScreenUnlock -> screenUnlock()
21292142
is Command.ShowFaviconsPrompt -> showFaviconsPrompt()
@@ -3012,16 +3025,15 @@ class BrowserTabFragment :
30123025
lifecycleScope.launch(dispatchers.main()) {
30133026
webViewClient.configureWebView(
30143027
it,
3015-
object : JsMessageCallback() {
3028+
object : WebViewCompatMessageCallback {
30163029
override fun process(
30173030
featureName: String,
30183031
method: String,
30193032
id: String?,
30203033
data: JSONObject?,
3034+
onResponse: (JSONObject) -> Unit,
30213035
) {
3022-
viewModel.processJsCallbackMessage(featureName, method, id, data, isActiveCustomTab()) {
3023-
it.url
3024-
}
3036+
viewModel.webViewCompatProcessJsCallbackMessage(featureName, method, id, data, onResponse)
30253037
}
30263038
},
30273039
)

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.ShowWebContent
162162
import com.duckduckgo.app.browser.commands.Command.ShowWebPageTitle
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
@@ -3666,6 +3667,23 @@ class BrowserTabViewModel @Inject constructor(
36663667
)
36673668
}
36683669

3670+
fun webViewCompatProcessJsCallbackMessage(
3671+
featureName: String,
3672+
method: String,
3673+
id: String?,
3674+
data: JSONObject?,
3675+
onResponse: (JSONObject) -> Unit,
3676+
) {
3677+
when (method) {
3678+
"webShare" -> if (id != null && data != null) {
3679+
webViewCompatWebShare(featureName, method, id, data, onResponse)
3680+
}
3681+
"addDebugFlag" -> {
3682+
site?.debugFlags = (site?.debugFlags ?: listOf()).toMutableList().plus(featureName)?.toList()
3683+
}
3684+
}
3685+
}
3686+
36693687
fun processJsCallbackMessage(
36703688
featureName: String,
36713689
method: String,
@@ -3752,6 +3770,18 @@ class BrowserTabViewModel @Inject constructor(
37523770
}
37533771
}
37543772

3773+
private fun webViewCompatWebShare(
3774+
featureName: String,
3775+
method: String,
3776+
id: String,
3777+
data: JSONObject,
3778+
onResponse: (JSONObject) -> Unit,
3779+
) {
3780+
viewModelScope.launch(dispatchers.main()) {
3781+
command.value = WebViewCompatWebShareRequest(JsCallbackData(data, featureName, method, id), onResponse)
3782+
}
3783+
}
3784+
37553785
private fun permissionsQuery(
37563786
featureName: String,
37573787
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)