Skip to content

Commit fc41ae7

Browse files
committed
Support message responses
1 parent 3df0ac2 commit fc41ae7

File tree

14 files changed

+275
-36
lines changed

14 files changed

+275
-36
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ import com.duckduckgo.history.api.NavigationHistory
8787
import com.duckduckgo.js.messaging.api.AddDocumentStartJavaScriptPlugin
8888
import com.duckduckgo.js.messaging.api.JsMessageCallback
8989
import com.duckduckgo.js.messaging.api.WebMessagingPlugin
90+
import com.duckduckgo.js.messaging.api.WebViewCompatMessageCallback
9091
import com.duckduckgo.privacy.config.api.AmpLinks
9192
import com.duckduckgo.subscriptions.api.Subscriptions
9293
import com.duckduckgo.user.agent.api.ClientBrandHintProvider
@@ -351,7 +352,7 @@ class BrowserWebViewClientTest {
351352
@Test
352353
fun whenConfigureWebViewThenInjectJsCode() {
353354
assertEquals(0, fakeAddDocumentStartJavaScriptPlugins.plugin.countInitted)
354-
val mockCallback = mock<JsMessageCallback>()
355+
val mockCallback = mock<WebViewCompatMessageCallback>()
355356
testee.configureWebView(DuckDuckGoWebView(context), mockCallback)
356357
assertEquals(1, fakeAddDocumentStartJavaScriptPlugins.plugin.countInitted)
357358
}
@@ -360,15 +361,15 @@ class BrowserWebViewClientTest {
360361
@Test
361362
fun whenConfigureWebViewThenAddWebMessageListener() {
362363
assertFalse(fakeMessagingPlugins.plugin.registered)
363-
val mockCallback = mock<JsMessageCallback>()
364+
val mockCallback = mock<WebViewCompatMessageCallback>()
364365
testee.configureWebView(DuckDuckGoWebView(context), mockCallback)
365366
assertTrue(fakeMessagingPlugins.plugin.registered)
366367
}
367368

368369
@UiThreadTest
369370
@Test
370371
fun whenDestroyThenRemoveWebMessageListener() = runTest {
371-
val mockCallback = mock<JsMessageCallback>()
372+
val mockCallback = mock<WebViewCompatMessageCallback>()
372373
val webView = DuckDuckGoWebView(context)
373374
testee.configureWebView(webView, mockCallback)
374375
assertTrue(fakeMessagingPlugins.plugin.registered)
@@ -1342,7 +1343,7 @@ class BrowserWebViewClientTest {
13421343
}
13431344

13441345
override fun register(
1345-
jsMessageCallback: JsMessageCallback,
1346+
jsMessageCallback: WebViewCompatMessageCallback,
13461347
webView: WebView,
13471348
) {
13481349
registered = true

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: 5 additions & 4 deletions
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,10 +469,10 @@ class BrowserWebViewClient @Inject constructor(
468469
webView.settings.mediaPlaybackRequiresUserGesture = mediaPlayback.doesMediaPlaybackRequireUserGestureForUrl(url)
469470
}
470471

471-
fun configureWebView(webView: DuckDuckGoWebView, callback: JsMessageCallback) {
472-
addDocumentStartJavascriptPlugins.getPlugins().forEach { plugin ->
473-
plugin.addDocumentStartJavaScript(webView)
474-
}
472+
fun configureWebView(webView: DuckDuckGoWebView, callback: WebViewCompatMessageCallback) {
473+
addDocumentStartJavascriptPlugins.getPlugins().forEach { plugin ->
474+
plugin.addDocumentStartJavaScript(webView)
475+
}
475476

476477
webMessagingPlugins.getPlugins().forEach { plugin ->
477478
plugin.register(callback, webView)

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
/**

0 commit comments

Comments
 (0)