Skip to content

Commit b2ab847

Browse files
committed
Add native flags
1 parent 1566341 commit b2ab847

File tree

4 files changed

+281
-36
lines changed

4 files changed

+281
-36
lines changed

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

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,10 @@ class BrowserTabFragment :
12571257
private fun onBrowserMenuButtonPressed() {
12581258
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
12591259
viewModel.onBrowserMenuClicked(isCustomTab = isActiveCustomTab())
1260+
1261+
lifecycleScope.launch {
1262+
webViewCompatTestHelper.onBrowserMenuButtonPressed(webView)
1263+
}
12601264
}
12611265

12621266
private fun onOmnibarPrivacyShieldButtonPressed() {
@@ -3250,7 +3254,10 @@ class BrowserTabFragment :
32503254
)
32513255
configureWebViewForBlobDownload(it)
32523256
lifecycleScope.launch {
3253-
webViewCompatTestHelper.configureWebViewForWebViewCompatTest(it)
3257+
webViewCompatTestHelper.configureWebViewForWebViewCompatTest(
3258+
it,
3259+
isBlobDownloadWebViewFeatureEnabled(it),
3260+
)
32543261
}
32553262
configureWebViewForAutofill(it)
32563263
printInjector.addJsInterface(it) { viewModel.printFromWebView() }
@@ -3378,7 +3385,13 @@ class BrowserTabFragment :
33783385
private fun configureWebViewForBlobDownload(webView: DuckDuckGoWebView) {
33793386
lifecycleScope.launch(dispatchers.main()) {
33803387
if (isBlobDownloadWebViewFeatureEnabled(webView)) {
3381-
val script = blobDownloadScript()
3388+
val useDedicatedWebViewCompatMessageListener = webViewCompatTestHelper.useDedicatedWebMessageListener()
3389+
3390+
val script = if (useDedicatedWebViewCompatMessageListener) {
3391+
blobDownloadScript()
3392+
} else {
3393+
blobDownloadScriptForWebViewCompatTest()
3394+
}
33823395
WebViewCompat.addDocumentStartJavaScript(webView, script, setOf("*"))
33833396

33843397
webViewCompatWrapper.addWebMessageListener(
@@ -3403,6 +3416,13 @@ class BrowserTabFragment :
34033416
.md5()
34043417
.toString()
34053418
viewModel.saveReplyProxyForBlobDownload(sourceOrigin.toString(), replyProxy, locationRef)
3419+
} else if (!useDedicatedWebViewCompatMessageListener && message.data?.startsWith("webViewCompat") == true) {
3420+
lifecycleScope.launch {
3421+
webViewCompatTestHelper.handleWebViewCompatMessage(
3422+
message = message,
3423+
replyProxy = replyProxy,
3424+
)
3425+
}
34063426
}
34073427
}
34083428
},
@@ -3422,6 +3442,61 @@ class BrowserTabFragment :
34223442
}
34233443
}
34243444

3445+
private fun blobDownloadScriptForWebViewCompatTest(): String {
3446+
val script =
3447+
"""
3448+
(function() {
3449+
3450+
const urlToBlobCollection = {};
3451+
3452+
const original_createObjectURL = URL.createObjectURL;
3453+
3454+
URL.createObjectURL = function () {
3455+
const blob = arguments[0];
3456+
const url = original_createObjectURL.call(this, ...arguments);
3457+
if (blob instanceof Blob) {
3458+
urlToBlobCollection[url] = blob;
3459+
}
3460+
return url;
3461+
}
3462+
3463+
function blobToBase64DataUrl(blob) {
3464+
return new Promise((resolve, reject) => {
3465+
const reader = new FileReader();
3466+
reader.onloadend = function() {
3467+
resolve(reader.result);
3468+
}
3469+
reader.onerror = function() {
3470+
reject(new Error('Failed to read Blob object'));
3471+
}
3472+
reader.readAsDataURL(blob);
3473+
});
3474+
}
3475+
3476+
const pingMessage = 'Ping:' + window.location.href;
3477+
window.ddgBlobDownloadObj.postMessage(pingMessage);
3478+
console.log('Sent ping message for blob downloads: ' + pingMessage);
3479+
3480+
window.ddgBlobDownloadObj.addEventListener('message', function(event) {
3481+
if (event.data.startsWith('blob:')) {
3482+
console.log(event.data);
3483+
const blob = urlToBlobCollection[event.data];
3484+
if (blob) {
3485+
blobToBase64DataUrl(blob).then((dataUrl) => {
3486+
console.log('Sending data URL back to native ' + dataUrl);
3487+
window.ddgBlobDownloadObj.postMessage(dataUrl);
3488+
});
3489+
} else {
3490+
console.log('No Blob found for URL: ' + event.data);
3491+
}
3492+
}
3493+
});
3494+
})();
3495+
""".trimIndent()
3496+
3497+
return script
3498+
}
3499+
34253500
private fun blobDownloadScript(): String {
34263501
val script =
34273502
"""

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

Lines changed: 157 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616

1717
package com.duckduckgo.app.browser
1818

19+
import android.annotation.SuppressLint
20+
import androidx.core.net.toUri
21+
import androidx.lifecycle.findViewTreeLifecycleOwner
22+
import androidx.lifecycle.lifecycleScope
23+
import androidx.webkit.JavaScriptReplyProxy
24+
import androidx.webkit.WebMessageCompat
25+
import androidx.webkit.WebViewCompat
1926
import com.duckduckgo.app.browser.webview.WebViewCompatFeature
2027
import com.duckduckgo.app.browser.webview.WebViewCompatFeatureSettings
2128
import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper
@@ -24,15 +31,27 @@ import com.duckduckgo.di.scopes.FragmentScope
2431
import com.squareup.anvil.annotations.ContributesBinding
2532
import com.squareup.moshi.Moshi
2633
import dagger.SingleInstanceIn
34+
import kotlinx.coroutines.delay
35+
import kotlinx.coroutines.launch
2736
import kotlinx.coroutines.withContext
2837
import javax.inject.Inject
2938

3039
private const val delay = "\$DELAY$"
3140
private const val postInitialPing = "\$POST_INITIAL_PING$"
3241
private const val replyToNativeMessages = "\$REPLY_TO_NATIVE_MESSAGES$"
42+
private const val objectName = "\$OBJECT_NAME$"
3343

3444
interface WebViewCompatTestHelper {
35-
suspend fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView)
45+
suspend fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView, isBlobDownloadWebViewFeatureEnabled: Boolean)
46+
suspend fun handleWebViewCompatMessage(
47+
message: WebMessageCompat,
48+
replyProxy: JavaScriptReplyProxy,
49+
)
50+
51+
suspend fun useDedicatedWebMessageListener(): Boolean
52+
53+
suspend fun onPageStarted(webView: DuckDuckGoWebView?)
54+
suspend fun onBrowserMenuButtonPressed(webView: DuckDuckGoWebView?)
3655
}
3756

3857
@ContributesBinding(FragmentScope::class)
@@ -44,23 +63,152 @@ class RealWebViewCompatTestHelper @Inject constructor(
4463
moshi: Moshi,
4564
) : WebViewCompatTestHelper {
4665

66+
private var proxy: JavaScriptReplyProxy? = null
67+
4768
private val adapter = moshi.adapter(WebViewCompatFeatureSettings::class.java)
4869

49-
override suspend fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView) {
50-
val script = withContext(dispatchers.io()) {
70+
data class WebViewCompatConfig(
71+
val settings: WebViewCompatFeatureSettings?,
72+
val dedicatedMessageListener: Boolean,
73+
val replyToInitialPing: Boolean,
74+
val jsSendsInitialPing: Boolean,
75+
val jsRepliesToNativeMessages: Boolean,
76+
)
77+
78+
private var cachedConfig: WebViewCompatConfig? = null
79+
80+
private suspend fun getWebViewCompatConfig(): WebViewCompatConfig? {
81+
cachedConfig?.let { return it }
82+
83+
return withContext(dispatchers.io()) {
5184
if (!webViewCompatFeature.self().isEnabled()) return@withContext null
5285

53-
val webViewCompatSettings = webViewCompatFeature.self().getSettings()?.let {
54-
adapter.fromJson(it)
86+
WebViewCompatConfig(
87+
settings = webViewCompatFeature.self().getSettings()?.let {
88+
adapter.fromJson(it)
89+
},
90+
dedicatedMessageListener = webViewCompatFeature.dedicatedMessageListener().isEnabled(),
91+
replyToInitialPing = webViewCompatFeature.replyToInitialPing().isEnabled(),
92+
jsSendsInitialPing = webViewCompatFeature.jsSendsInitialPing().isEnabled(),
93+
jsRepliesToNativeMessages = webViewCompatFeature.jsRepliesToNativeMessages().isEnabled(),
94+
).also {
95+
cachedConfig = it
5596
}
56-
webView.resources?.openRawResource(R.raw.webviewcompat_test_script)?.bufferedReader().use { it?.readText() }.orEmpty()
57-
.replace(delay, webViewCompatSettings?.jsInitialPingDelay?.toString() ?: "0")
58-
.replace(postInitialPing, webViewCompatFeature.jsSendsInitialPing().isEnabled().toString())
59-
.replace(replyToNativeMessages, webViewCompatFeature.jsRepliesToNativeMessages().isEnabled().toString())
97+
}
98+
}
99+
100+
override suspend fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView, isBlobDownloadWebViewFeatureEnabled: Boolean) {
101+
val config = getWebViewCompatConfig() ?: return
102+
103+
val useDedicatedListener = config.dedicatedMessageListener || !isBlobDownloadWebViewFeatureEnabled
104+
105+
val script = withContext(dispatchers.io()) {
106+
webView.context.resources?.openRawResource(R.raw.webviewcompat_test_script)
107+
?.bufferedReader().use { it?.readText() }
108+
?.replace(delay, config.settings?.jsInitialPingDelay?.toString() ?: "0")
109+
?.replace(postInitialPing, config.jsSendsInitialPing.toString())
110+
?.replace(replyToNativeMessages, config.jsRepliesToNativeMessages.toString())
111+
?.replace(objectName, if (useDedicatedListener) "webViewCompatTestObj" else "ddgBlobDownloadObj")
60112
} ?: return
61113

62114
withContext(dispatchers.main()) {
63115
webViewCompatWrapper.addDocumentStartJavaScript(webView, script, setOf("*"))
116+
117+
if (useDedicatedListener) {
118+
webViewCompatWrapper.addWebMessageListener(
119+
webView,
120+
"webViewCompatTestObj",
121+
setOf("*"),
122+
) { view, message, sourceOrigin, isMainFrame, replyProxy ->
123+
webView.findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
124+
handleWebViewCompatMessage(message, replyProxy)
125+
}
126+
}
127+
}
128+
}
129+
}
130+
131+
@SuppressLint("PostMessageUsage", "RequiresFeature")
132+
private suspend fun postMessage(string: String) {
133+
withContext(dispatchers.main()) {
134+
proxy?.postMessage("PageStarted")
135+
}
136+
}
137+
138+
@SuppressLint("PostMessageUsage", "RequiresFeature")
139+
override suspend fun handleWebViewCompatMessage(
140+
message: WebMessageCompat,
141+
replyProxy: JavaScriptReplyProxy,
142+
) {
143+
withContext(dispatchers.io()) {
144+
if (message.data?.startsWith("webViewCompat Ping:") != true) return@withContext
145+
getWebViewCompatConfig()?.let { cfg ->
146+
proxy = replyProxy
147+
if (cfg.replyToInitialPing) {
148+
cfg.settings?.initialPingDelay?.takeIf { it > 0 }?.let {
149+
delay(it)
150+
}
151+
withContext(dispatchers.main()) {
152+
replyProxy.postMessage("Pong from Native")
153+
}
154+
}
155+
}
156+
}
157+
}
158+
159+
override suspend fun useDedicatedWebMessageListener(): Boolean {
160+
return withContext(dispatchers.io()) { getWebViewCompatConfig()?.dedicatedMessageListener ?: true }
161+
}
162+
163+
@SuppressLint("PostMessageUsage", "RequiresFeature")
164+
override suspend fun onPageStarted(webView: DuckDuckGoWebView?) {
165+
withContext(dispatchers.main()) {
166+
val flags = withContext(dispatchers.io()) {
167+
object {
168+
val sendMessageOnPageStarted = webViewCompatFeature.sendMessageOnPageStarted().isEnabled()
169+
val sendMessagesUsingReplyProxy = webViewCompatFeature.sendMessagesUsingReplyProxy().isEnabled()
170+
}
171+
}
172+
173+
if (!flags.sendMessageOnPageStarted) return@withContext
174+
175+
if (flags.sendMessagesUsingReplyProxy) {
176+
postMessage("PageStarted")
177+
} else {
178+
webView?.url?.let {
179+
WebViewCompat.postWebMessage(
180+
webView,
181+
WebMessageCompat("PageStarted"),
182+
it.toUri(),
183+
)
184+
}
185+
}
186+
}
187+
}
188+
189+
@SuppressLint("RequiresFeature")
190+
override suspend fun onBrowserMenuButtonPressed(webView: DuckDuckGoWebView?) {
191+
withContext(dispatchers.main()) {
192+
val flags = withContext(dispatchers.io()) {
193+
object {
194+
val sendMessageOnContextMenuOpen = webViewCompatFeature.sendMessageOnContextMenuOpen().isEnabled()
195+
val sendMessagesUsingReplyProxy = webViewCompatFeature.sendMessagesUsingReplyProxy().isEnabled()
196+
}
197+
}
198+
199+
if (!flags.sendMessageOnContextMenuOpen) return@withContext
200+
201+
if (flags.sendMessagesUsingReplyProxy) {
202+
postMessage("ContextMenuOpened")
203+
} else {
204+
webView?.url?.let {
205+
WebViewCompat.postWebMessage(
206+
webView,
207+
WebMessageCompat("ContextMenuOpened"),
208+
it.toUri(),
209+
)
210+
}
211+
}
64212
}
65213
}
66214
}

app/src/main/java/com/duckduckgo/app/browser/webview/WebViewCompatFeature.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,24 @@ interface WebViewCompatFeature {
3535

3636
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
3737
fun jsRepliesToNativeMessages(): Toggle
38+
39+
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
40+
fun replyToInitialPing(): Toggle
41+
42+
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
43+
fun dedicatedMessageListener(): Toggle
44+
45+
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
46+
fun sendMessageOnContextMenuOpen(): Toggle
47+
48+
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
49+
fun sendMessageOnPageStarted(): Toggle
50+
51+
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
52+
fun sendMessagesUsingReplyProxy(): Toggle
3853
}
3954

4055
data class WebViewCompatFeatureSettings(
4156
val jsInitialPingDelay: Long = 0,
57+
val initialPingDelay: Long = 0,
4258
)

0 commit comments

Comments
 (0)