Skip to content

Commit 52448d0

Browse files
authored
Add native flags (#7047)
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1211755269770778?focus=true ### Description ### Steps to test this PR _Feature 1_ - [ ] Apply [patch](https://duckduckgo-my.sharepoint.com/:u:/p/cbarreiro/EQzRDGHt8VVNicoRG-pU560BBVo07vUHvuprNNcneF9Qgg?e=SJ0f8h) - [ ] Fresh install - [ ] Filter logs by "Cris" - [ ] Load http://privacy-test-pages.site/security/badware/malware-download.html and refresh page - [ ] Check logs for - [ ] `Blob downloads listener received WebViewCompat message: webViewCompat Ping:http://privacy-test-pages.site/security/badware/malware-download.html 10ms`. Validates `jsSendsInitialPing` and `jsInitialPingDelay` - [ ] `delaying response by 10 ms`. Validates `initialPingDelay` - [ ] `Posting response Pong from Native`. Validates `replyToInitialPing` - [ ] `Sending message PageStarted using reply proxy`. Validates `sendMessageOnPageStarted` and `sendMessagesUsingReplyProxy` - [ ] `Blob downloads listener received WebViewCompat message: webViewCompat PageStarted from ddgBlobDownloadObj`. Validates `useBlobDownloadsMessageListener` and `jsRepliesToNativeMessages` - [ ] Open context menu - [ ] Check logs for - [ ] `Sending message ContextMenuOpened using reply proxy`. Validates `sendMessageOnContextMenuOpen` - [ ] `Blob downloads listener received WebViewCompat message: webViewCompat ContextMenuOpened from ddgBlobDownloadObj`. Validates `useBlobDownloadsMessageListener` and `jsRepliesToNativeMessages` - [ ] Test blob downloads still work _Feature 2_ - [ ] Apply [patch ](https://duckduckgo-my.sharepoint.com/:u:/p/cbarreiro/EeK6GQN0aK9Fola1TnxLnnABPCzKOQpmcEN5jd5JUNSDuQ?e=xdS2SB) - [ ] Fresh install - [ ] Filter logs by "Cris" - [ ] Load http://privacy-test-pages.site/security/badware/malware-download.html and refresh page - [ ] Check logs for - [ ] `webViewCompat message received: webViewCompat Ping:http://privacy-test-pages.site/security/badware/malware-download.html 10ms`. Validates `initialPingDelay` 10ms, `jsSendsInitialPing` enabled, and `useBlobDownloadsMessageListener` disabled - [ ] No log for `delaying response by 10 ms` nor `Posting response Pong from Native`. Validates `replyToInitialPing` disabled - [ ] No log for `Sending message PageStarted...`. Validates `sendMessageOnPageStarted` disabled - [ ] Open context menu - [ ] Check no logs for `Sending message ContextMenuOpened...`. Validates `sendMessageOnContextMenuOpen` disabled _Feature 3_ - [ ] Apply [patch](https://duckduckgo-my.sharepoint.com/:u:/p/cbarreiro/EZsVUIl3CNVFkgn4cJYwuLQBdrNHa8ONEHu_264GHUYoVg?e=ZMA438) - [ ] Fresh install - [ ] Filter logs by "Cris" - [ ] Load http://privacy-test-pages.site/security/badware/malware-download.html and refresh page - [ ] Check logs for - [ ] `webViewCompat message received: webViewCompat Ping:http://privacy-test-pages.site/security/badware/malware-download.html 10ms`. Validates `initialPingDelay` 10ms, `jsSendsInitialPing` enabled, and `useBlobDownloadsMessageListener` disabled - [ ] No log for `delaying response by 10 ms` nor `Posting response Pong from Native`. Validates `replyToInitialPing` disabled - [ ] No log for `Sending message PageStarted...`. Validates `sendMessageOnPageStarted` disabled - [ ] Open context menu - [ ] Check logs for `Sending message ContextMenuOpened not using reply proxy`. Validates `sendMessageOnContextMenuOpen` enabled and `sendMessagesUsingReplyProxy` disabled ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)|
1 parent 0770edf commit 52448d0

File tree

6 files changed

+289
-37
lines changed

6 files changed

+289
-37
lines changed

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,16 @@ class BrowserTabFragment :
12601260
private fun onBrowserMenuButtonPressed() {
12611261
contentScopeScripts.sendSubscriptionEvent(createBreakageReportingEventData())
12621262
viewModel.onBrowserMenuClicked(isCustomTab = isActiveCustomTab())
1263+
1264+
lifecycleScope.launch {
1265+
webViewCompatTestHelper.onBrowserMenuButtonPressed(webView)
1266+
}
1267+
}
1268+
1269+
private fun onPageStarted() {
1270+
lifecycleScope.launch {
1271+
webViewCompatTestHelper.onPageStarted(webView)
1272+
}
12631273
}
12641274

12651275
private fun onOmnibarPrivacyShieldButtonPressed() {
@@ -2344,6 +2354,7 @@ class BrowserTabFragment :
23442354

23452355
is Command.SubmitChat -> duckChat.openDuckChatWithAutoPrompt(it.query)
23462356
is Command.EnqueueCookiesAnimation -> enqueueCookiesAnimation(it.isCosmetic)
2357+
is Command.PageStarted -> onPageStarted()
23472358
}
23482359
}
23492360

@@ -3254,7 +3265,10 @@ class BrowserTabFragment :
32543265
)
32553266
configureWebViewForBlobDownload(it)
32563267
lifecycleScope.launch {
3257-
webViewCompatTestHelper.configureWebViewForWebViewCompatTest(it)
3268+
webViewCompatTestHelper.configureWebViewForWebViewCompatTest(
3269+
it,
3270+
isBlobDownloadWebViewFeatureEnabled(it),
3271+
)
32583272
}
32593273
configureWebViewForAutofill(it)
32603274
printInjector.addJsInterface(it) { viewModel.printFromWebView() }
@@ -3382,7 +3396,13 @@ class BrowserTabFragment :
33823396
private fun configureWebViewForBlobDownload(webView: DuckDuckGoWebView) {
33833397
lifecycleScope.launch(dispatchers.main()) {
33843398
if (isBlobDownloadWebViewFeatureEnabled(webView)) {
3385-
val script = blobDownloadScript()
3399+
val webViewCompatUsesBlobDownloadsMessageListener = webViewCompatTestHelper.useBlobDownloadsMessageListener()
3400+
3401+
val script = if (!webViewCompatUsesBlobDownloadsMessageListener) {
3402+
blobDownloadScript()
3403+
} else {
3404+
webViewCompatTestHelper.blobDownloadScriptForWebViewCompatTest()
3405+
}
33863406
WebViewCompat.addDocumentStartJavaScript(webView, script, setOf("*"))
33873407

33883408
webViewCompatWrapper.addWebMessageListener(
@@ -3407,6 +3427,14 @@ class BrowserTabFragment :
34073427
.md5()
34083428
.toString()
34093429
viewModel.saveReplyProxyForBlobDownload(sourceOrigin.toString(), replyProxy, locationRef)
3430+
} else if (webViewCompatUsesBlobDownloadsMessageListener && message.data?.startsWith("webViewCompat") == true) {
3431+
lifecycleScope.launch {
3432+
webViewCompatTestHelper.handleWebViewCompatMessage(
3433+
message = message,
3434+
replyProxy = replyProxy,
3435+
isMainFrame = isMainFrame,
3436+
)
3437+
}
34103438
}
34113439
}
34123440
},

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1972,6 +1972,8 @@ class BrowserTabViewModel @Inject constructor(
19721972
maliciousSiteBlocked = false,
19731973
)
19741974
navigationStateChanged(webViewNavigationState)
1975+
1976+
command.postValue(Command.PageStarted)
19751977
}
19761978

19771979
override fun onSitePermissionRequested(

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

Lines changed: 210 additions & 10 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,30 @@ 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+
isMainFrame: Boolean,
50+
)
51+
52+
suspend fun useBlobDownloadsMessageListener(): Boolean
53+
54+
suspend fun onPageStarted(webView: DuckDuckGoWebView?)
55+
suspend fun onBrowserMenuButtonPressed(webView: DuckDuckGoWebView?)
56+
57+
fun blobDownloadScriptForWebViewCompatTest(): String
3658
}
3759

3860
@ContributesBinding(FragmentScope::class)
@@ -44,23 +66,201 @@ class RealWebViewCompatTestHelper @Inject constructor(
4466
moshi: Moshi,
4567
) : WebViewCompatTestHelper {
4668

69+
private var proxy: JavaScriptReplyProxy? = null
70+
4771
private val adapter = moshi.adapter(WebViewCompatFeatureSettings::class.java)
4872

49-
override suspend fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView) {
50-
val script = withContext(dispatchers.io()) {
51-
if (!webViewCompatFeature.self().isEnabled()) return@withContext null
73+
data class WebViewCompatConfig(
74+
val settings: WebViewCompatFeatureSettings?,
75+
val useBlobDownloadsMessageListener: Boolean,
76+
val replyToInitialPing: Boolean,
77+
val jsSendsInitialPing: Boolean,
78+
val jsRepliesToNativeMessages: Boolean,
79+
val sendMessageOnPageStarted: Boolean,
80+
val sendMessageOnContextMenuOpen: Boolean,
81+
val sendMessagesUsingReplyProxy: Boolean,
82+
)
83+
84+
private var cachedConfig: WebViewCompatConfig? = null
5285

53-
val webViewCompatSettings = webViewCompatFeature.self().getSettings()?.let {
54-
adapter.fromJson(it)
86+
private suspend fun getWebViewCompatConfig(): WebViewCompatConfig {
87+
cachedConfig?.let { return it }
88+
89+
return withContext(dispatchers.io()) {
90+
WebViewCompatConfig(
91+
settings = webViewCompatFeature.self().getSettings()?.let {
92+
adapter.fromJson(it)
93+
},
94+
useBlobDownloadsMessageListener = webViewCompatFeature.useBlobDownloadsMessageListener().isEnabled(),
95+
replyToInitialPing = webViewCompatFeature.replyToInitialPing().isEnabled(),
96+
jsSendsInitialPing = webViewCompatFeature.jsSendsInitialPing().isEnabled(),
97+
jsRepliesToNativeMessages = webViewCompatFeature.jsRepliesToNativeMessages().isEnabled(),
98+
sendMessageOnPageStarted = webViewCompatFeature.sendMessageOnPageStarted().isEnabled(),
99+
sendMessageOnContextMenuOpen = webViewCompatFeature.sendMessageOnContextMenuOpen().isEnabled(),
100+
sendMessagesUsingReplyProxy = webViewCompatFeature.sendMessagesUsingReplyProxy().isEnabled(),
101+
).also {
102+
cachedConfig = it
55103
}
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())
104+
}
105+
}
106+
107+
override suspend fun configureWebViewForWebViewCompatTest(webView: DuckDuckGoWebView, isBlobDownloadWebViewFeatureEnabled: Boolean) {
108+
val config = getWebViewCompatConfig()
109+
110+
val useDedicatedListener = !config.useBlobDownloadsMessageListener || !isBlobDownloadWebViewFeatureEnabled
111+
112+
val script = withContext(dispatchers.io()) {
113+
if (!webViewCompatFeature.self().isEnabled()) return@withContext null
114+
webView.context.resources?.openRawResource(R.raw.webviewcompat_test_script)
115+
?.bufferedReader().use { it?.readText() }
116+
?.replace(delay, config.settings?.jsInitialPingDelay?.toString() ?: "0")
117+
?.replace(postInitialPing, config.jsSendsInitialPing.toString())
118+
?.replace(replyToNativeMessages, config.jsRepliesToNativeMessages.toString())
119+
?.replace(objectName, if (useDedicatedListener) "webViewCompatTestObj" else "ddgBlobDownloadObj")
60120
} ?: return
61121

62122
withContext(dispatchers.main()) {
63123
webViewCompatWrapper.addDocumentStartJavaScript(webView, script, setOf("*"))
124+
125+
if (useDedicatedListener) {
126+
webViewCompatWrapper.addWebMessageListener(
127+
webView,
128+
"webViewCompatTestObj",
129+
setOf("*"),
130+
) { view, message, sourceOrigin, isMainFrame, replyProxy ->
131+
webView.findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
132+
handleWebViewCompatMessage(message, replyProxy, isMainFrame)
133+
}
134+
}
135+
}
136+
}
137+
}
138+
139+
@SuppressLint("PostMessageUsage", "RequiresFeature")
140+
override suspend fun handleWebViewCompatMessage(
141+
message: WebMessageCompat,
142+
replyProxy: JavaScriptReplyProxy,
143+
isMainFrame: Boolean,
144+
) {
145+
withContext(dispatchers.io()) {
146+
if (message.data?.startsWith("webViewCompat Ping:") != true) return@withContext
147+
val cfg = getWebViewCompatConfig()
148+
if (isMainFrame) {
149+
proxy = replyProxy
150+
}
151+
if (cfg.replyToInitialPing) {
152+
cfg.settings?.initialPingDelay?.takeIf { it > 0 }?.let {
153+
delay(it)
154+
}
155+
withContext(dispatchers.main()) {
156+
replyProxy.postMessage("Pong from Native")
157+
}
158+
}
64159
}
65160
}
161+
162+
override suspend fun useBlobDownloadsMessageListener(): Boolean {
163+
return withContext(dispatchers.io()) { getWebViewCompatConfig().useBlobDownloadsMessageListener }
164+
}
165+
166+
@SuppressLint("PostMessageUsage", "RequiresFeature")
167+
override suspend fun onPageStarted(webView: DuckDuckGoWebView?) {
168+
withContext(dispatchers.main()) {
169+
val config = getWebViewCompatConfig()
170+
171+
if (!config.sendMessageOnPageStarted) return@withContext
172+
173+
val messageData = "PageStarted"
174+
175+
if (config.sendMessagesUsingReplyProxy) {
176+
proxy?.postMessage(messageData)
177+
} else {
178+
webView?.url?.let {
179+
WebViewCompat.postWebMessage(
180+
webView,
181+
WebMessageCompat(messageData),
182+
it.toUri(),
183+
)
184+
}
185+
}
186+
}
187+
}
188+
189+
@SuppressLint("RequiresFeature", "PostMessageUsage")
190+
override suspend fun onBrowserMenuButtonPressed(webView: DuckDuckGoWebView?) {
191+
withContext(dispatchers.main()) {
192+
val config = getWebViewCompatConfig()
193+
194+
if (!config.sendMessageOnContextMenuOpen) return@withContext
195+
196+
val messageData = "ContextMenuOpened"
197+
198+
if (config.sendMessagesUsingReplyProxy) {
199+
proxy?.postMessage(messageData)
200+
} else {
201+
webView?.url?.let {
202+
WebViewCompat.postWebMessage(
203+
webView,
204+
WebMessageCompat(messageData),
205+
it.toUri(),
206+
)
207+
}
208+
}
209+
}
210+
}
211+
212+
override fun blobDownloadScriptForWebViewCompatTest(): String {
213+
val script =
214+
"""
215+
(function() {
216+
217+
const urlToBlobCollection = {};
218+
219+
const original_createObjectURL = URL.createObjectURL;
220+
221+
URL.createObjectURL = function () {
222+
const blob = arguments[0];
223+
const url = original_createObjectURL.call(this, ...arguments);
224+
if (blob instanceof Blob) {
225+
urlToBlobCollection[url] = blob;
226+
}
227+
return url;
228+
}
229+
230+
function blobToBase64DataUrl(blob) {
231+
return new Promise((resolve, reject) => {
232+
const reader = new FileReader();
233+
reader.onloadend = function() {
234+
resolve(reader.result);
235+
}
236+
reader.onerror = function() {
237+
reject(new Error('Failed to read Blob object'));
238+
}
239+
reader.readAsDataURL(blob);
240+
});
241+
}
242+
243+
const pingMessage = 'Ping:' + window.location.href;
244+
window.ddgBlobDownloadObj.postMessage(pingMessage);
245+
console.log('Sent ping message for blob downloads: ' + pingMessage);
246+
247+
window.ddgBlobDownloadObj.addEventListener('message', function(event) {
248+
if (event.data.startsWith('blob:')) {
249+
console.log(event.data);
250+
const blob = urlToBlobCollection[event.data];
251+
if (blob) {
252+
blobToBase64DataUrl(blob).then((dataUrl) => {
253+
console.log('Sending data URL back to native ' + dataUrl);
254+
window.ddgBlobDownloadObj.postMessage(dataUrl);
255+
});
256+
} else {
257+
console.log('No Blob found for URL: ' + event.data);
258+
}
259+
}
260+
});
261+
})();
262+
""".trimIndent()
263+
264+
return script
265+
}
66266
}

app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,4 +502,5 @@ sealed class Command {
502502
data class EnqueueCookiesAnimation(
503503
val isCosmetic: Boolean,
504504
) : Command()
505+
data object PageStarted : Command()
505506
}

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 useBlobDownloadsMessageListener(): 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)