Skip to content

Commit a9c3baf

Browse files
committed
Extract blob download in iframes logic to make it reusable
1 parent 1b11c38 commit a9c3baf

File tree

19 files changed

+645
-300
lines changed

19 files changed

+645
-300
lines changed

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,6 @@ import kotlin.collections.List
311311
import kotlin.collections.Map
312312
import kotlin.collections.MutableMap
313313
import kotlin.collections.any
314-
import kotlin.collections.component1
315-
import kotlin.collections.component2
316314
import kotlin.collections.contains
317315
import kotlin.collections.drop
318316
import kotlin.collections.emptyList
@@ -322,7 +320,6 @@ import kotlin.collections.filterNot
322320
import kotlin.collections.firstOrNull
323321
import kotlin.collections.forEach
324322
import kotlin.collections.isNotEmpty
325-
import kotlin.collections.iterator
326323
import kotlin.collections.map
327324
import kotlin.collections.mapOf
328325
import kotlin.collections.minus
@@ -333,7 +330,6 @@ import kotlin.collections.set
333330
import kotlin.collections.setOf
334331
import kotlin.collections.take
335332
import kotlin.collections.toList
336-
import kotlin.collections.toMutableMap
337333
import kotlinx.coroutines.CoroutineScope
338334
import kotlinx.coroutines.ExperimentalCoroutinesApi
339335
import kotlinx.coroutines.FlowPreview

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability
2424
import com.duckduckgo.browser.api.WebViewVersionProvider
2525
import com.duckduckgo.common.utils.DispatcherProvider
2626
import com.duckduckgo.common.utils.extensions.compareSemanticVersion
27-
import com.duckduckgo.di.scopes.FragmentScope
27+
import com.duckduckgo.di.scopes.AppScope
2828
import com.squareup.anvil.annotations.ContributesBinding
2929
import javax.inject.Inject
3030
import kotlinx.coroutines.withContext
3131

32-
@ContributesBinding(FragmentScope::class)
32+
@ContributesBinding(AppScope::class)
3333
class RealWebViewCapabilityChecker @Inject constructor(
3434
private val dispatchers: DispatcherProvider,
3535
private val webViewVersionProvider: WebViewVersionProvider,
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright (c) 2024 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.downloader
18+
19+
import android.annotation.SuppressLint
20+
import android.net.Uri
21+
import android.webkit.WebView
22+
import androidx.webkit.JavaScriptReplyProxy
23+
import androidx.webkit.WebViewCompat
24+
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker
25+
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability
26+
import com.duckduckgo.app.browser.webview.WebViewBlobDownloadFeature
27+
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
28+
import com.duckduckgo.browser.api.download.WebViewBlobDownloader
29+
import com.duckduckgo.common.utils.DispatcherProvider
30+
import com.duckduckgo.di.scopes.AppScope
31+
import com.squareup.anvil.annotations.ContributesBinding
32+
import javax.inject.Inject
33+
import kotlinx.coroutines.withContext
34+
35+
@ContributesBinding(AppScope::class)
36+
class WebViewBlobDownloaderModernImpl @Inject constructor(
37+
private val webViewBlobDownloadFeature: WebViewBlobDownloadFeature,
38+
private val dispatchers: DispatcherProvider,
39+
private val webViewCapabilityChecker: WebViewCapabilityChecker,
40+
private val androidBrowserConfig: AndroidBrowserConfigFeature,
41+
42+
) : WebViewBlobDownloader {
43+
44+
private val replyProxyMap = mutableMapOf<String, JavaScriptReplyProxy>()
45+
46+
// Map<String, Map<String, JavaScriptReplyProxy>>() = Map<Origin, Map<location.href, JavaScriptReplyProxy>>()
47+
private val fixedReplyProxyMap = mutableMapOf<String, Map<String, JavaScriptReplyProxy>>()
48+
49+
@SuppressLint("RequiresFeature")
50+
override suspend fun addBlobDownloadSupport(webView: WebView) {
51+
if (isBlobDownloadWebViewFeatureEnabled()) {
52+
WebViewCompat.addDocumentStartJavaScript(webView, script, setOf("*"))
53+
}
54+
}
55+
56+
@SuppressLint("RequiresFeature")
57+
override suspend fun convertBlobToDataUri(blobUrl: String) {
58+
if (withContext(dispatchers.io()) { androidBrowserConfig.fixBlobDownloadWithIframes().isEnabled() }) {
59+
for ((key, proxies) in fixedReplyProxyMap) {
60+
if (sameOrigin(blobUrl.removePrefix("blob:"), key)) {
61+
for (replyProxy in proxies.values) {
62+
replyProxy.postMessage(blobUrl)
63+
}
64+
return
65+
}
66+
}
67+
} else {
68+
for ((key, value) in replyProxyMap) {
69+
if (sameOrigin(blobUrl.removePrefix("blob:"), key)) {
70+
value.postMessage(blobUrl)
71+
return
72+
}
73+
}
74+
}
75+
}
76+
77+
override suspend fun storeReplyProxy(
78+
originUrl: String,
79+
replyProxy: JavaScriptReplyProxy,
80+
locationHref: String?,
81+
) {
82+
if (androidBrowserConfig.fixBlobDownloadWithIframes().isEnabled()) {
83+
val frameProxies = fixedReplyProxyMap[originUrl]?.toMutableMap() ?: mutableMapOf()
84+
// if location.href is not passed, we fall back to origin
85+
val safeLocationHref = locationHref ?: originUrl
86+
frameProxies[safeLocationHref] = replyProxy
87+
fixedReplyProxyMap[originUrl] = frameProxies
88+
} else {
89+
replyProxyMap[originUrl] = replyProxy
90+
}
91+
}
92+
93+
private fun sameOrigin(firstUrl: String, secondUrl: String): Boolean {
94+
return kotlin.runCatching {
95+
val firstUri = Uri.parse(firstUrl)
96+
val secondUri = Uri.parse(secondUrl)
97+
98+
firstUri.host == secondUri.host && firstUri.scheme == secondUri.scheme && firstUri.port == secondUri.port
99+
}.getOrNull() ?: return false
100+
}
101+
102+
override fun clearReplyProxies() {
103+
fixedReplyProxyMap.clear()
104+
replyProxyMap.clear()
105+
}
106+
107+
private suspend fun isBlobDownloadWebViewFeatureEnabled(): Boolean {
108+
return withContext(dispatchers.io()) { webViewBlobDownloadFeature.self().isEnabled() } &&
109+
webViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener) &&
110+
webViewCapabilityChecker.isSupported(WebViewCapability.DocumentStartJavaScript)
111+
}
112+
113+
companion object {
114+
private val script = """
115+
window.__url_to_blob_collection = {};
116+
117+
const original_createObjectURL = URL.createObjectURL;
118+
119+
URL.createObjectURL = function () {
120+
const blob = arguments[0];
121+
const url = original_createObjectURL.call(this, ...arguments);
122+
if (blob instanceof Blob) {
123+
__url_to_blob_collection[url] = blob;
124+
}
125+
return url;
126+
}
127+
128+
function blobToBase64DataUrl(blob) {
129+
return new Promise((resolve, reject) => {
130+
const reader = new FileReader();
131+
reader.onloadend = function() {
132+
resolve(reader.result);
133+
}
134+
reader.onerror = function() {
135+
reject(new Error('Failed to read Blob object'));
136+
}
137+
reader.readAsDataURL(blob);
138+
});
139+
}
140+
141+
const pingMessage = 'Ping:' + window.location.href
142+
ddgBlobDownloadObj.postMessage(pingMessage)
143+
144+
ddgBlobDownloadObj.onmessage = function(event) {
145+
if (event.data.startsWith('blob:')) {
146+
const blob = window.__url_to_blob_collection[event.data];
147+
if (blob) {
148+
blobToBase64DataUrl(blob).then((dataUrl) => {
149+
ddgBlobDownloadObj.postMessage(dataUrl);
150+
});
151+
}
152+
}
153+
}
154+
""".trimIndent()
155+
}
156+
}

autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillScreens.kt

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@
1616

1717
package com.duckduckgo.autofill.api
1818

19-
import android.os.Parcelable
2019
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
2120
import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
22-
import kotlinx.parcelize.Parcelize
2321

2422
sealed interface AutofillScreens {
2523

@@ -55,23 +53,6 @@ sealed interface AutofillScreens {
5553
data object AutofillImportViaGooglePasswordManagerScreen : ActivityParams {
5654
private fun readResolve(): Any = AutofillImportViaGooglePasswordManagerScreen
5755
}
58-
59-
sealed interface Result : Parcelable {
60-
61-
companion object {
62-
const val RESULT_KEY = "importResult"
63-
const val RESULT_KEY_DETAILS = "importResultDetails"
64-
}
65-
66-
@Parcelize
67-
data class Success(val importedCount: Int) : Result
68-
69-
@Parcelize
70-
data class UserCancelled(val stage: String) : Result
71-
72-
@Parcelize
73-
data object Error : Result
74-
}
7556
}
7657
}
7758

autofill/autofill-impl/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ plugins {
1818
id 'com.android.library'
1919
id 'kotlin-android'
2020
id 'com.squareup.anvil'
21+
id 'kotlin-parcelize'
2122
}
2223

2324
apply from: "$rootProject.projectDir/gradle/android-library.gradle"

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/CsvPasswordImporter.kt

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,28 @@
1717
package com.duckduckgo.autofill.impl.importing
1818

1919
import android.net.Uri
20+
import android.os.Parcelable
2021
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
22+
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter.ParseResult
23+
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter.ParseResult.Success
2124
import com.duckduckgo.common.utils.DispatcherProvider
2225
import com.duckduckgo.di.scopes.AppScope
2326
import com.squareup.anvil.annotations.ContributesBinding
2427
import javax.inject.Inject
2528
import kotlinx.coroutines.withContext
29+
import kotlinx.parcelize.Parcelize
2630

2731
interface CsvPasswordImporter {
28-
suspend fun readCsv(blob: String): List<LoginCredentials>
29-
suspend fun readCsv(fileUri: Uri): List<LoginCredentials>
32+
suspend fun readCsv(blob: String): ParseResult
33+
suspend fun readCsv(fileUri: Uri): ParseResult
34+
35+
sealed interface ParseResult : Parcelable {
36+
@Parcelize
37+
data class Success(val numberPasswordsInSource: Int, val loginCredentialsToImport: List<LoginCredentials>) : ParseResult
38+
39+
@Parcelize
40+
data object Error : ParseResult
41+
}
3042
}
3143

3244
@ContributesBinding(AppScope::class)
@@ -39,30 +51,30 @@ class GooglePasswordManagerCsvPasswordImporter @Inject constructor(
3951
private val blobDecoder: GooglePasswordBlobDecoder,
4052
) : CsvPasswordImporter {
4153

42-
override suspend fun readCsv(blob: String): List<LoginCredentials> {
54+
override suspend fun readCsv(blob: String): ParseResult {
4355
return kotlin.runCatching {
4456
withContext(dispatchers.io()) {
4557
val csv = blobDecoder.decode(blob)
46-
importPasswords(csv)
58+
convertToLoginCredentials(csv)
4759
}
48-
}.getOrElse { emptyList() }
60+
}.getOrElse { ParseResult.Error }
4961
}
5062

51-
override suspend fun readCsv(fileUri: Uri): List<LoginCredentials> {
63+
override suspend fun readCsv(fileUri: Uri): ParseResult {
5264
return kotlin.runCatching {
5365
withContext(dispatchers.io()) {
5466
val csv = fileReader.readCsvFile(fileUri)
55-
importPasswords(csv)
67+
convertToLoginCredentials(csv)
5668
}
57-
}.getOrElse { emptyList() }
69+
}.getOrElse { ParseResult.Error }
5870
}
5971

60-
private suspend fun importPasswords(csv: String): List<LoginCredentials> {
72+
private suspend fun convertToLoginCredentials(csv: String): Success {
6173
val allPasswords = parser.parseCsv(csv)
6274
val dedupedPasswords = allPasswords.distinct()
6375
val validPasswords = filterValidPasswords(dedupedPasswords)
6476
val normalizedDomains = domainNameNormalizer.normalizeDomains(validPasswords)
65-
return normalizedDomains
77+
return Success(allPasswords.size, normalizedDomains)
6678
}
6779

6880
private fun filterValidPasswords(passwords: List<LoginCredentials>): List<LoginCredentials> {

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/PasswordImporter.kt

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,55 @@
1616

1717
package com.duckduckgo.autofill.impl.importing
1818

19+
import android.os.Parcelable
1920
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
2021
import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult
22+
import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult.Finished
23+
import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult.InProgress
2124
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
2225
import com.duckduckgo.common.utils.DispatcherProvider
2326
import com.duckduckgo.di.scopes.AppScope
2427
import com.squareup.anvil.annotations.ContributesBinding
28+
import dagger.SingleInstanceIn
2529
import javax.inject.Inject
30+
import kotlinx.coroutines.flow.Flow
31+
import kotlinx.coroutines.flow.MutableSharedFlow
2632
import kotlinx.coroutines.withContext
33+
import kotlinx.parcelize.Parcelize
2734

2835
interface PasswordImporter {
29-
suspend fun importPasswords(importList: List<LoginCredentials>): ImportResult
36+
suspend fun importPasswords(importList: List<LoginCredentials>)
37+
fun getImportStatus(): Flow<ImportResult>
3038

31-
data class ImportResult(val savedCredentialIds: List<Long>, val duplicatedPasswords: List<LoginCredentials>)
39+
sealed interface ImportResult : Parcelable {
40+
41+
@Parcelize
42+
data class InProgress(
43+
val savedCredentialIds: List<Long>,
44+
val duplicatedPasswords: List<LoginCredentials>,
45+
val importListSize: Int,
46+
) : ImportResult
47+
48+
@Parcelize
49+
data class Finished(
50+
val savedCredentialIds: List<Long>,
51+
val duplicatedPasswords: List<LoginCredentials>,
52+
val importListSize: Int,
53+
) : ImportResult
54+
}
3255
}
3356

57+
@SingleInstanceIn(AppScope::class)
3458
@ContributesBinding(AppScope::class)
3559
class PasswordImporterImpl @Inject constructor(
3660
private val existingPasswordMatchDetector: ExistingPasswordMatchDetector,
3761
private val autofillStore: InternalAutofillStore,
3862
private val dispatchers: DispatcherProvider,
3963
) : PasswordImporter {
4064

41-
override suspend fun importPasswords(importList: List<LoginCredentials>): ImportResult {
65+
private val _importStatus = MutableSharedFlow<ImportResult>(replay = 1)
66+
67+
override suspend fun importPasswords(importList: List<LoginCredentials>) {
4268
return withContext(dispatchers.io()) {
4369
val savedCredentialIds = mutableListOf<Long>()
4470
val duplicatedPasswords = mutableListOf<LoginCredentials>()
@@ -53,9 +79,15 @@ class PasswordImporterImpl @Inject constructor(
5379
} else {
5480
duplicatedPasswords.add(it)
5581
}
82+
83+
_importStatus.emit(InProgress(savedCredentialIds, duplicatedPasswords, importList.size))
5684
}
5785

58-
ImportResult(savedCredentialIds, duplicatedPasswords)
86+
_importStatus.emit(Finished(savedCredentialIds, duplicatedPasswords, importList.size))
5987
}
6088
}
89+
90+
override fun getImportStatus(): Flow<ImportResult> {
91+
return _importStatus
92+
}
6193
}

0 commit comments

Comments
 (0)