Skip to content

Commit 333213a

Browse files
committed
Handle dedicated message listener flags
1 parent e5c9160 commit 333213a

File tree

2 files changed

+282
-34
lines changed

2 files changed

+282
-34
lines changed

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

Lines changed: 238 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
350350
import kotlinx.coroutines.CoroutineScope
351351
import kotlinx.coroutines.Job
352352
import kotlinx.coroutines.SupervisorJob
353+
import kotlinx.coroutines.async
353354
import kotlinx.coroutines.delay
354355
import kotlinx.coroutines.flow.cancellable
355356
import kotlinx.coroutines.flow.collectLatest
@@ -1248,9 +1249,61 @@ class BrowserTabFragment :
12481249
viewModel.onFireMenuSelected()
12491250
}
12501251

1252+
@SuppressLint("PostMessageUsage")
1253+
private fun onPageStarted() {
1254+
postBreakageReportingEvent()
1255+
1256+
lifecycleScope.launch(dispatchers.main()) {
1257+
val flags = withContext(dispatchers.io()) {
1258+
object {
1259+
val sendMessageOnPageStarted = true // webViewCompatFeature.sendMessageOnPageStarted().isEnabled()
1260+
val sendMessagesUsingReplyProxy = true // webViewCompatFeature.sendMessagesUsingReplyProxy().isEnabled()
1261+
}
1262+
}
1263+
1264+
if (!flags.sendMessageOnPageStarted) return@launch
1265+
1266+
if (flags.sendMessagesUsingReplyProxy) {
1267+
proxy?.postMessage("PageStarted")
1268+
} else {
1269+
webView?.url?.let {
1270+
WebViewCompat.postWebMessage(
1271+
webView,
1272+
WebMessageCompat("PageStarted"),
1273+
it.toUri(),
1274+
)
1275+
}
1276+
}
1277+
}
1278+
}
1279+
1280+
@SuppressLint("PostMessageUsage")
12511281
private fun onBrowserMenuButtonPressed() {
12521282
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
12531283
viewModel.onBrowserMenuClicked(isCustomTab = isActiveCustomTab())
1284+
1285+
lifecycleScope.launch(dispatchers.main()) {
1286+
val flags = withContext(dispatchers.io()) {
1287+
object {
1288+
val sendMessageOnContextMenuOpen = webViewCompatFeature.sendMessageOnContextMenuOpen().isEnabled()
1289+
val sendMessagesUsingReplyProxy = webViewCompatFeature.sendMessagesUsingReplyProxy().isEnabled()
1290+
}
1291+
}
1292+
1293+
if (!flags.sendMessageOnContextMenuOpen) return@launch
1294+
1295+
if (flags.sendMessagesUsingReplyProxy) {
1296+
proxy?.postMessage("ContextMenuOpened")
1297+
} else {
1298+
webView?.url?.let {
1299+
WebViewCompat.postWebMessage(
1300+
webView,
1301+
WebMessageCompat("ContextMenuOpened"),
1302+
it.toUri(),
1303+
)
1304+
}
1305+
}
1306+
}
12541307
}
12551308

12561309
private fun onOmnibarPrivacyShieldButtonPressed() {
@@ -2316,6 +2369,7 @@ class BrowserTabFragment :
23162369

23172370
is Command.SubmitChat -> duckChat.openDuckChatWithAutoPrompt(it.query)
23182371
is Command.EnqueueCookiesAnimation -> enqueueCookiesAnimation(it.isCosmetic)
2372+
is Command.PageStarted -> onPageStarted()
23192373
}
23202374
}
23212375

@@ -3353,56 +3407,156 @@ class BrowserTabFragment :
33533407
private val delay = "\$DELAY$"
33543408
private val postInitialPing = "\$POST_INITIAL_PING$"
33553409
private val replyToNativeMessages = "\$REPLY_TO_NATIVE_MESSAGES$"
3410+
private val objectName = "\$OBJECT_NAME$"
3411+
3412+
private data class WebViewCompatConfig(
3413+
val settings: WebViewCompatFeatureSettings?,
3414+
val dedicatedMessageListener: Boolean,
3415+
val replyToInitialPing: Boolean,
3416+
val jsSendsInitialPing: Boolean,
3417+
val jsRepliesToNativeMessages: Boolean,
3418+
)
3419+
3420+
private val webViewCompatConfigDeferred by lazy {
3421+
lifecycleScope.async(dispatchers.io()) {
3422+
if (!webViewCompatFeature.self().isEnabled()) return@async null
3423+
3424+
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
3425+
val adapter = moshi.adapter(WebViewCompatFeatureSettings::class.java)
3426+
3427+
WebViewCompatConfig(
3428+
settings = webViewCompatFeature.self().getSettings()?.let {
3429+
adapter.fromJson(it)
3430+
},
3431+
dedicatedMessageListener = webViewCompatFeature.dedicatedMessageListener().isEnabled(),
3432+
replyToInitialPing = webViewCompatFeature.replyToInitialPing().isEnabled(),
3433+
jsSendsInitialPing = webViewCompatFeature.jsSendsInitialPing().isEnabled(),
3434+
jsRepliesToNativeMessages = webViewCompatFeature.jsRepliesToNativeMessages().isEnabled(),
3435+
).also {
3436+
logcat("Cris") { "WebViewCompatConfig: $it" }
3437+
}
3438+
}
3439+
}
3440+
3441+
@SuppressLint("PostMessageUsage")
3442+
private fun processWebViewCompatPingMessage(
3443+
messageData: String?,
3444+
config: WebViewCompatConfig,
3445+
replyProxy: JavaScriptReplyProxy,
3446+
) {
3447+
if (messageData?.startsWith("webViewCompat Ping:") != true) return
3448+
3449+
proxy = replyProxy
3450+
if (config.replyToInitialPing) {
3451+
lifecycleScope.launch {
3452+
config.settings?.initialPingDelay?.takeIf { it > 0 }?.let {
3453+
delay(it)
3454+
}
3455+
replyProxy.postMessage("Pong from Native")
3456+
}
3457+
}
3458+
}
33563459

33573460
private fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView) {
33583461
lifecycleScope.launch(dispatchers.main()) {
3462+
val config = webViewCompatConfigDeferred.await() ?: return@launch
3463+
3464+
val useDedicatedListener = config.dedicatedMessageListener || !isBlobDownloadWebViewFeatureEnabled(webView)
3465+
33593466
val script = withContext(dispatchers.io()) {
3360-
if (!webViewCompatFeature.self().isEnabled()) return@withContext null
3467+
context?.resources?.openRawResource(R.raw.webviewcompat_test_script)
3468+
?.bufferedReader().use { it?.readText() }.orEmpty()
3469+
.replace(delay, config.settings?.jsInitialPingDelay?.toString() ?: "0")
3470+
.replace(postInitialPing, config.jsSendsInitialPing.toString())
3471+
.replace(replyToNativeMessages, config.jsRepliesToNativeMessages.toString())
3472+
.replace(objectName, if (useDedicatedListener) "webViewCompatTestObj" else "ddgBlobDownloadObj")
3473+
}
33613474

3362-
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
3363-
val adapter = moshi.adapter(WebViewCompatFeatureSettings::class.java)
3364-
val webViewCompatSettings = webViewCompatFeature.self().getSettings()?.let {
3365-
adapter.fromJson(it)
3366-
}
3367-
context?.resources?.openRawResource(R.raw.webviewcompat_test_script)?.bufferedReader().use { it?.readText() }.orEmpty()
3368-
.replace(delay, webViewCompatSettings?.jsInitialPingDelay?.toString() ?: "0")
3369-
.replace(postInitialPing, webViewCompatFeature.jsSendsInitialPing().isEnabled().toString())
3370-
.replace(replyToNativeMessages, webViewCompatFeature.jsRepliesToNativeMessages().isEnabled().toString())
3371-
} ?: return@launch
3475+
// logcat("Cris") {"Script: $script"}
33723476

33733477
webViewCompatWrapper.addDocumentStartJavaScript(webView, script, setOf("*"))
3478+
3479+
if (useDedicatedListener) {
3480+
webViewCompatWrapper.addWebMessageListener(
3481+
webView,
3482+
"webViewCompatTestObj",
3483+
setOf("*"),
3484+
object : WebViewCompat.WebMessageListener {
3485+
@SuppressLint("PostMessageUsage")
3486+
override fun onPostMessage(
3487+
view: WebView,
3488+
message: WebMessageCompat,
3489+
sourceOrigin: Uri,
3490+
isMainFrame: Boolean,
3491+
replyProxy: JavaScriptReplyProxy,
3492+
) {
3493+
logcat("Cris") { "Received message from WebViewCompat test script: ${message.data}" }
3494+
processWebViewCompatPingMessage(message.data, config, replyProxy)
3495+
}
3496+
},
3497+
)
3498+
}
33743499
}
33753500
}
33763501

33773502
@SuppressLint("AddDocumentStartJavaScriptUsage")
33783503
private fun configureWebViewForBlobDownload(webView: DuckDuckGoWebView) {
33793504
lifecycleScope.launch(dispatchers.main()) {
33803505
if (isBlobDownloadWebViewFeatureEnabled(webView)) {
3381-
val script = blobDownloadScript()
3506+
val config = webViewCompatConfigDeferred.await()
3507+
3508+
val script = if (config == null || config.dedicatedMessageListener) {
3509+
blobDownloadScriptOriginal()
3510+
} else {
3511+
blobDownloadScript()
3512+
}
33823513
WebViewCompat.addDocumentStartJavaScript(webView, script, setOf("*"))
33833514

3515+
logcat("Cris") { "Adding message listener for blob downloads" }
3516+
3517+
val processWebViewCompatMessages = config != null && !config.dedicatedMessageListener
3518+
33843519
webViewCompatWrapper.addWebMessageListener(
33853520
webView,
33863521
"ddgBlobDownloadObj",
33873522
setOf("*"),
33883523
object : WebViewCompat.WebMessageListener {
3524+
@SuppressLint("PostMessageUsage")
33893525
override fun onPostMessage(
33903526
view: WebView,
33913527
message: WebMessageCompat,
33923528
sourceOrigin: Uri,
33933529
isMainFrame: Boolean,
33943530
replyProxy: JavaScriptReplyProxy,
33953531
) {
3532+
logcat("Cris") { "Blob downloads: ${message.data}" }
3533+
33963534
if (message.data?.startsWith("data:") == true) {
33973535
requestFileDownload(message.data!!, null, "", true)
33983536
} else if (message.data?.startsWith("Ping:") == true) {
3537+
logcat("Cris") { "Blob downloads proxy: storing..." }
33993538
val locationRef =
34003539
message.data
34013540
.toString()
34023541
.encode()
34033542
.md5()
34043543
.toString()
34053544
viewModel.saveReplyProxyForBlobDownload(sourceOrigin.toString(), replyProxy, locationRef)
3545+
} else if (processWebViewCompatMessages && message.data?.startsWith("webViewCompat") == true) {
3546+
config?.let { cfg ->
3547+
if (message.data?.startsWith("webViewCompat Ping:") ?: false) {
3548+
proxy = replyProxy
3549+
if (cfg.replyToInitialPing) {
3550+
lifecycleScope.launch {
3551+
cfg.settings?.initialPingDelay?.takeIf { it > 0 }?.let {
3552+
delay(it)
3553+
}
3554+
replyProxy.postMessage("Pong from Native")
3555+
}
3556+
}
3557+
}
3558+
}
3559+
logcat("Cris") { "WebViewCompat message processed on blob downloads" }
34063560
}
34073561
}
34083562
},
@@ -3423,6 +3577,78 @@ class BrowserTabFragment :
34233577
}
34243578

34253579
private fun blobDownloadScript(): String {
3580+
val script =
3581+
"""
3582+
(function() {
3583+
// Capture the injected object immediately
3584+
const ddgBlob = window.ddgBlobDownloadObj;
3585+
3586+
if (!ddgBlob) {
3587+
console.error('ddgBlobDownloadObj not found');
3588+
return;
3589+
}
3590+
3591+
try {
3592+
Object.defineProperty(window, 'ddgBlobDownloadObj', {
3593+
value: ddgBlob,
3594+
writable: false,
3595+
configurable: false,
3596+
});
3597+
} catch (e) {
3598+
console.warn('Could not protect ddgBlobDownloadObj:', e);
3599+
}
3600+
3601+
const urlToBlobCollection = {};
3602+
3603+
const original_createObjectURL = URL.createObjectURL;
3604+
3605+
URL.createObjectURL = function () {
3606+
const blob = arguments[0];
3607+
const url = original_createObjectURL.call(this, ...arguments);
3608+
if (blob instanceof Blob) {
3609+
urlToBlobCollection[url] = blob;
3610+
}
3611+
return url;
3612+
}
3613+
3614+
function blobToBase64DataUrl(blob) {
3615+
return new Promise((resolve, reject) => {
3616+
const reader = new FileReader();
3617+
reader.onloadend = function() {
3618+
resolve(reader.result);
3619+
}
3620+
reader.onerror = function() {
3621+
reject(new Error('Failed to read Blob object'));
3622+
}
3623+
reader.readAsDataURL(blob);
3624+
});
3625+
}
3626+
3627+
const pingMessage = 'Ping:' + window.location.href;
3628+
ddgBlob.postMessage(pingMessage);
3629+
console.log('Sent ping message for blob downloads: ' + pingMessage);
3630+
3631+
ddgBlob.addEventListener('message', function(event) {
3632+
if (event.data.startsWith('blob:')) {
3633+
console.log(event.data);
3634+
const blob = urlToBlobCollection[event.data];
3635+
if (blob) {
3636+
blobToBase64DataUrl(blob).then((dataUrl) => {
3637+
console.log('Sending data URL back to native ' + dataUrl);
3638+
ddgBlob.postMessage(dataUrl);
3639+
});
3640+
} else {
3641+
console.log('No Blob found for URL: ' + event.data);
3642+
}
3643+
}
3644+
});
3645+
})();
3646+
""".trimIndent()
3647+
3648+
return script
3649+
}
3650+
3651+
private fun blobDownloadScriptOriginal(): String {
34263652
val script =
34273653
"""
34283654
window.__url_to_blob_collection = {};
Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,51 @@
1-
const supportedMessages = ["ContextMenuOpened", "PageStarted"];
1+
(function() {
2+
// Capture the injected object immediately
3+
const ddgObj = window.$OBJECT_NAME$;
4+
5+
if (!ddgObj) {
6+
console.error('$OBJECT_NAME$ not found');
7+
return;
8+
}
9+
10+
// Make it read-only to prevent overwrites (don't delete - other scripts may need it)
11+
try {
12+
Object.defineProperty(window, '$OBJECT_NAME$', {
13+
value: ddgObj,
14+
writable: false,
15+
configurable: false,
16+
});
17+
} catch (e) {
18+
console.warn('Could not protect $OBJECT_NAME$:', e);
19+
}
20+
21+
const supportedMessages = ["ContextMenuOpened", "PageStarted"];
222

3-
const delay = $DELAY$;
4-
const postInitialPing = $POST_INITIAL_PING$;
5-
const replyToNativeMessages = $REPLY_TO_NATIVE_MESSAGES$;
23+
const delay = $DELAY$;
24+
const postInitialPing = $POST_INITIAL_PING$;
25+
const replyToNativeMessages = $REPLY_TO_NATIVE_MESSAGES$;
26+
const messagePrefix = 'webViewCompat '
627

7-
const webViewCompatPingMessage = 'Ping:' + window.location.href + ' ' + delay + 'ms'
28+
const webViewCompatPingMessage = messagePrefix + 'Ping:' + window.location.href + ' ' + delay + 'ms'
829

930

10-
if (postInitialPing) {
11-
setTimeout(() => {
12-
webViewCompatTestObj.postMessage(webViewCompatPingMessage)
13-
}, delay)
14-
}
31+
if (postInitialPing) {
32+
setTimeout(() => {
33+
ddgObj.postMessage(webViewCompatPingMessage)
34+
}, delay)
35+
}
1536

1637

17-
webViewCompatTestObj.onmessage = function(event) {
18-
console.log("webViewCompatTestObj received", event.data)
19-
if (replyToNativeMessages && supportedMessages.includes(event.data)) {
20-
webViewCompatTestObj.postMessage(event.data + " from webViewCompatTestObj")
21-
}
22-
}
38+
ddgObj.addEventListener('message', function(event) {
39+
console.log("$OBJECT_NAME$ received", event.data)
40+
if (replyToNativeMessages && supportedMessages.includes(event.data)) {
41+
ddgObj.postMessage(messagePrefix + event.data + " from $OBJECT_NAME$")
42+
}
43+
});
2344

24-
window.onmessage = function(event) {
25-
console.log("window received", event.data)
26-
if (replyToNativeMessages && supportedMessages.includes(event.data)) {
27-
webViewCompatTestObj.postMessage(event.data + " from window")
28-
}
29-
}
45+
window.addEventListener('message', function(event) {
46+
console.log("window received", event.data)
47+
if (replyToNativeMessages && supportedMessages.includes(event.data)) {
48+
ddgObj.postMessage(messagePrefix + event.data + " from window")
49+
}
50+
});
51+
})();

0 commit comments

Comments
 (0)