@@ -350,6 +350,7 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
350350import kotlinx.coroutines.CoroutineScope
351351import kotlinx.coroutines.Job
352352import kotlinx.coroutines.SupervisorJob
353+ import kotlinx.coroutines.async
353354import kotlinx.coroutines.delay
354355import kotlinx.coroutines.flow.cancellable
355356import 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 = {};
0 commit comments