Skip to content

Commit 4c29255

Browse files
committed
Support message responses
1 parent cc13d8c commit 4c29255

File tree

9 files changed

+233
-15
lines changed

9 files changed

+233
-15
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.LoadingViewState
176176
import com.duckduckgo.app.browser.viewstate.OmnibarViewState
177177
import com.duckduckgo.app.browser.viewstate.PrivacyShieldViewState
178178
import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState
179+
import com.duckduckgo.app.browser.webshare.AdsjsWebShareChooser
179180
import com.duckduckgo.app.browser.webshare.WebShareChooser
180181
import com.duckduckgo.app.browser.webview.WebContentDebugging
181182
import com.duckduckgo.app.browser.webview.WebViewBlobDownloadFeature
@@ -295,6 +296,7 @@ import com.duckduckgo.duckchat.api.inputscreen.InputScreenActivityResultCodes
295296
import com.duckduckgo.duckchat.api.inputscreen.InputScreenActivityResultParams
296297
import com.duckduckgo.duckplayer.api.DuckPlayer
297298
import com.duckduckgo.duckplayer.api.DuckPlayerSettingsNoParams
299+
import com.duckduckgo.js.messaging.api.AdsjsJsMessageCallback
298300
import com.duckduckgo.js.messaging.api.AdsjsMessaging
299301
import com.duckduckgo.js.messaging.api.JsCallbackData
300302
import com.duckduckgo.js.messaging.api.JsMessageCallback
@@ -884,6 +886,13 @@ class BrowserTabFragment :
884886
contentScopeScripts.onResponse(it)
885887
}
886888

889+
private var currentWebShareReplyCallback: ((JSONObject) -> Unit)? = null
890+
891+
private val adsJsWebShareLauncher = registerForActivityResult(AdsjsWebShareChooser()) { 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()
@@ -2140,6 +2149,10 @@ class BrowserTabFragment :
21402149
is Command.SendResponseToJs -> contentScopeScripts.onResponse(it.data)
21412150
is Command.SendResponseToDuckPlayer -> duckPlayerScripts.onResponse(it.data)
21422151
is Command.WebShareRequest -> webShareRequest.launch(it.data)
2152+
is Command.AdsjsWebShareRequest -> {
2153+
currentWebShareReplyCallback = it.onResponse
2154+
adsJsWebShareLauncher.launch(it.data)
2155+
}
21432156
is Command.ScreenLock -> screenLock(it.data)
21442157
is Command.ScreenUnlock -> screenUnlock()
21452158
is Command.ShowFaviconsPrompt -> showFaviconsPrompt()
@@ -3015,16 +3028,15 @@ class BrowserTabFragment :
30153028
webViewClient.triggerJSInit(it)
30163029
adsJsContentScopeScripts.register(
30173030
it,
3018-
object : JsMessageCallback() {
3031+
object : AdsjsJsMessageCallback() {
30193032
override fun process(
30203033
featureName: String,
30213034
method: String,
30223035
id: String?,
30233036
data: JSONObject?,
3037+
onResponse: (JSONObject) -> Unit,
30243038
) {
3025-
viewModel.processJsCallbackMessage(featureName, method, id, data, isActiveCustomTab()) {
3026-
it.url
3027-
}
3039+
viewModel.adsjsProcessJsCallbackMessage(featureName, method, id, data, onResponse)
30283040
}
30293041
},
30303042
)

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import com.duckduckgo.app.browser.certificates.BypassedSSLCertificatesRepository
6767
import com.duckduckgo.app.browser.certificates.remoteconfig.SSLCertificatesFeature
6868
import com.duckduckgo.app.browser.commands.Command
6969
import com.duckduckgo.app.browser.commands.Command.AddHomeShortcut
70+
import com.duckduckgo.app.browser.commands.Command.AdsjsWebShareRequest
7071
import com.duckduckgo.app.browser.commands.Command.AskToAutomateFireproofWebsite
7172
import com.duckduckgo.app.browser.commands.Command.AskToDisableLoginDetection
7273
import com.duckduckgo.app.browser.commands.Command.AskToFireproofWebsite
@@ -3635,6 +3636,20 @@ class BrowserTabViewModel @Inject constructor(
36353636
)
36363637
}
36373638

3639+
fun adsjsProcessJsCallbackMessage(
3640+
featureName: String,
3641+
method: String,
3642+
id: String?,
3643+
data: JSONObject?,
3644+
onResponse: (JSONObject) -> Unit,
3645+
) {
3646+
when (method) {
3647+
"webShare" -> if (id != null && data != null) {
3648+
adsjsWebShare(featureName, method, id, data, onResponse)
3649+
}
3650+
}
3651+
}
3652+
36383653
fun processJsCallbackMessage(
36393654
featureName: String,
36403655
method: String,
@@ -3721,6 +3736,18 @@ class BrowserTabViewModel @Inject constructor(
37213736
}
37223737
}
37233738

3739+
private fun adsjsWebShare(
3740+
featureName: String,
3741+
method: String,
3742+
id: String,
3743+
data: JSONObject,
3744+
onResponse: (JSONObject) -> Unit,
3745+
) {
3746+
viewModelScope.launch(dispatchers.main()) {
3747+
command.value = AdsjsWebShareRequest(JsCallbackData(data, featureName, method, id), onResponse)
3748+
}
3749+
}
3750+
37243751
private fun permissionsQuery(
37253752
featureName: String,
37263753
method: String,

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 AdsjsWebShareRequest(val data: JsCallbackData, val onResponse: (JSONObject) -> Unit) : Command()
252254
data class ScreenLock(val data: JsCallbackData) : Command()
253255
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 AdsjsWebShareChooser : 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+
}

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
@@ -16,8 +16,9 @@
1616

1717
package com.duckduckgo.contentscopescripts.api
1818

19+
import com.duckduckgo.js.messaging.api.AdsjsJsMessageCallback
1920
import com.duckduckgo.js.messaging.api.JsMessage
20-
import com.duckduckgo.js.messaging.api.JsMessageCallback
21+
import org.json.JSONObject
2122

2223
/**
2324
* Plugin interface for global message handlers that should always be processed
@@ -42,7 +43,8 @@ interface GlobalJsMessageHandler {
4243
*/
4344
fun process(
4445
jsMessage: JsMessage,
45-
jsMessageCallback: JsMessageCallback,
46+
jsMessageCallback: AdsjsJsMessageCallback,
47+
onResponse: (JSONObject) -> Unit,
4648
)
4749

4850
/**

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

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,25 @@ package com.duckduckgo.contentscopescripts.impl.messaging
1818

1919
import android.annotation.SuppressLint
2020
import android.webkit.WebView
21+
import androidx.webkit.JavaScriptReplyProxy
2122
import androidx.webkit.WebViewCompat
2223
import androidx.webkit.WebViewFeature
2324
import com.duckduckgo.common.utils.plugins.PluginPoint
2425
import com.duckduckgo.contentscopescripts.api.AdsjsContentScopeJsMessageHandlersPlugin
2526
import com.duckduckgo.contentscopescripts.api.GlobalContentScopeJsMessageHandlersPlugin
2627
import com.duckduckgo.di.scopes.ActivityScope
28+
import com.duckduckgo.js.messaging.api.AdsjsJsMessageCallback
2729
import com.duckduckgo.js.messaging.api.AdsjsMessaging
30+
import com.duckduckgo.js.messaging.api.JsCallbackData
2831
import com.duckduckgo.js.messaging.api.JsMessage
29-
import com.duckduckgo.js.messaging.api.JsMessageCallback
3032
import com.squareup.anvil.annotations.ContributesBinding
3133
import com.squareup.moshi.Moshi
3234
import javax.inject.Inject
3335
import javax.inject.Named
3436
import logcat.LogPriority.ERROR
3537
import logcat.asLog
3638
import logcat.logcat
39+
import org.json.JSONObject
3740

3841
@ContributesBinding(ActivityScope::class)
3942
@Named("AdsjsContentScopeScripts")
@@ -51,7 +54,8 @@ class AdsjsContentScopeMessaging @Inject constructor(
5154

5255
private fun process(
5356
message: String,
54-
jsMessageCallback: JsMessageCallback,
57+
jsMessageCallback: AdsjsJsMessageCallback,
58+
replyProxy: JavaScriptReplyProxy,
5559
) {
5660
try {
5761
val adapter = moshi.adapter(JsMessage::class.java)
@@ -64,13 +68,22 @@ class AdsjsContentScopeMessaging @Inject constructor(
6468
.map { it.getGlobalJsMessageHandler() }
6569
.filter { it.method == jsMessage.method }
6670
.forEach { handler ->
67-
handler.process(jsMessage, jsMessageCallback)
71+
handler.process(jsMessage, jsMessageCallback) {
72+
}
6873
}
6974

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

8194
// TODO: A/B this, don't register if the feature is not enabled
8295
@SuppressLint("AddWebMessageListenerUsage") // safeAddWebMessageListener belongs to app module
83-
override fun register(webView: WebView, jsMessageCallback: JsMessageCallback?) {
96+
override fun register(
97+
webView: WebView,
98+
jsMessageCallback: AdsjsJsMessageCallback?,
99+
) {
84100
if (jsMessageCallback == null) throw Exception("Callback cannot be null")
85101
this.webView = webView
86102

@@ -94,6 +110,7 @@ class AdsjsContentScopeMessaging @Inject constructor(
94110
process(
95111
message.data ?: "",
96112
jsMessageCallback,
113+
replyProxy,
97114
)
98115
}
99116
true
@@ -105,4 +122,21 @@ class AdsjsContentScopeMessaging @Inject constructor(
105122
false
106123
}
107124
}
125+
126+
private fun onResponse(response: JsCallbackData, replyProxy: JavaScriptReplyProxy) {
127+
runCatching {
128+
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
129+
val responseWithId = JSONObject().apply {
130+
put("id", response.id)
131+
put("result", response.params)
132+
put("featureName", response.featureName)
133+
put("context", context)
134+
}
135+
136+
replyProxy.postMessage(responseWithId.toString())
137+
} else {
138+
logcat(ERROR) { "WebMessageListener is not supported on this WebView" }
139+
}
140+
}
141+
}
108142
}

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
@@ -19,10 +19,11 @@ package com.duckduckgo.contentscopescripts.impl.messaging
1919
import com.duckduckgo.contentscopescripts.api.GlobalContentScopeJsMessageHandlersPlugin
2020
import com.duckduckgo.contentscopescripts.api.GlobalJsMessageHandler
2121
import com.duckduckgo.di.scopes.AppScope
22+
import com.duckduckgo.js.messaging.api.AdsjsJsMessageCallback
2223
import com.duckduckgo.js.messaging.api.JsMessage
23-
import com.duckduckgo.js.messaging.api.JsMessageCallback
2424
import com.squareup.anvil.annotations.ContributesMultibinding
2525
import javax.inject.Inject
26+
import org.json.JSONObject
2627

2728
@ContributesMultibinding(AppScope::class)
2829
class DebugFlagGlobalHandler @Inject constructor() : GlobalContentScopeJsMessageHandlersPlugin {
@@ -31,14 +32,16 @@ class DebugFlagGlobalHandler @Inject constructor() : GlobalContentScopeJsMessage
3132

3233
override fun process(
3334
jsMessage: JsMessage,
34-
jsMessageCallback: JsMessageCallback,
35+
jsMessageCallback: AdsjsJsMessageCallback,
36+
onResponse: (JSONObject) -> Unit,
3537
) {
3638
if (jsMessage.method == method) {
3739
jsMessageCallback.process(
3840
featureName = jsMessage.featureName,
3941
method = jsMessage.method,
4042
id = jsMessage.id,
4143
data = jsMessage.params,
44+
onResponse = onResponse,
4245
)
4346
}
4447
}

0 commit comments

Comments
 (0)