1616
1717package 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
1926import com.duckduckgo.app.browser.webview.WebViewCompatFeature
2027import com.duckduckgo.app.browser.webview.WebViewCompatFeatureSettings
2128import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper
@@ -24,15 +31,27 @@ import com.duckduckgo.di.scopes.FragmentScope
2431import com.squareup.anvil.annotations.ContributesBinding
2532import com.squareup.moshi.Moshi
2633import dagger.SingleInstanceIn
34+ import kotlinx.coroutines.delay
35+ import kotlinx.coroutines.launch
2736import kotlinx.coroutines.withContext
2837import javax.inject.Inject
2938
3039private const val delay = " \$ DELAY$"
3140private const val postInitialPing = " \$ POST_INITIAL_PING$"
3241private const val replyToNativeMessages = " \$ REPLY_TO_NATIVE_MESSAGES$"
42+ private const val objectName = " \$ OBJECT_NAME$"
3343
3444interface 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}
0 commit comments