Skip to content

Commit a039a47

Browse files
committed
Add webflow for importing bookmarks from Google
1 parent c3b2c4a commit a039a47

28 files changed

+11639
-4550
lines changed

autofill/autofill-impl/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
android:name=".importing.gpm.webflow.ImportGooglePasswordsWebFlowActivity"
2222
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|navigation|keyboard"
2323
android:exported="false" />
24+
<activity
25+
android:name=".importing.takeout.webflow.ImportGoogleBookmarksWebFlowActivity"
26+
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|navigation|keyboard"
27+
android:exported="false" />
2428
<activity
2529
android:name=".ui.credential.management.AutofillManagementActivity"
2630
android:configChanges="orientation|screenSize"
Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,41 @@
1717
package com.duckduckgo.autofill.impl.importing.gpm.webflow
1818

1919
import com.duckduckgo.autofill.impl.importing.gpm.feature.AutofillImportPasswordConfigStore
20+
import com.duckduckgo.autofill.impl.importing.takeout.store.BookmarkImportConfigStore
2021
import com.duckduckgo.common.utils.DispatcherProvider
2122
import com.duckduckgo.di.scopes.FragmentScope
2223
import com.squareup.anvil.annotations.ContributesBinding
2324
import java.io.BufferedReader
2425
import javax.inject.Inject
2526
import kotlinx.coroutines.withContext
2627

27-
interface PasswordImporterScriptLoader {
28-
suspend fun getScript(): String
28+
interface GoogleImporterScriptLoader {
29+
suspend fun getScriptForPasswordImport(): String
30+
suspend fun getScriptForBookmarkImport(): String
2931
}
3032

3133
@ContributesBinding(FragmentScope::class)
32-
class PasswordImporterCssScriptLoader @Inject constructor(
34+
class GoogleImporterScriptLoaderImpl @Inject constructor(
3335
private val dispatchers: DispatcherProvider,
34-
private val configStore: AutofillImportPasswordConfigStore,
35-
) : PasswordImporterScriptLoader {
36+
private val passwordConfigStore: AutofillImportPasswordConfigStore,
37+
private val bookmarkConfigStore: BookmarkImportConfigStore,
38+
) : GoogleImporterScriptLoader {
3639

3740
private lateinit var contentScopeJS: String
3841

39-
override suspend fun getScript(): String {
42+
override suspend fun getScriptForPasswordImport(): String {
4043
return withContext(dispatchers.io()) {
4144
getContentScopeJS()
42-
.replace(CONTENT_SCOPE_PLACEHOLDER, getContentScopeJson(loadSettingsJson()))
45+
.replace(CONTENT_SCOPE_PLACEHOLDER, getContentScopeScriptJson(loadSettingsJsonPassword()))
46+
.replace(USER_UNPROTECTED_DOMAINS_PLACEHOLDER, getUnprotectedDomainsJson())
47+
.replace(USER_PREFERENCES_PLACEHOLDER, getUserPreferencesJson())
48+
}
49+
}
50+
51+
override suspend fun getScriptForBookmarkImport(): String {
52+
return withContext(dispatchers.io()) {
53+
getContentScopeJS()
54+
.replace(CONTENT_SCOPE_PLACEHOLDER, getContentScopeScriptJson(loadSettingsJsonBookmark()))
4355
.replace(USER_UNPROTECTED_DOMAINS_PLACEHOLDER, getUnprotectedDomainsJson())
4456
.replace(USER_PREFERENCES_PLACEHOLDER, getUserPreferencesJson())
4557
}
@@ -49,10 +61,10 @@ class PasswordImporterCssScriptLoader @Inject constructor(
4961
* This enables the password import hints feature in C-S-S.
5062
* These settings are for enabling it; the check for whether it should be enabled or not is done elsewhere.
5163
*/
52-
private fun getContentScopeJson(settingsJson: String): String {
64+
private fun getContentScopeScriptJson(settingsJson: String): String {
5365
return """{
5466
"features":{
55-
"autofillPasswordImport" : {
67+
"autofillImport" : {
5668
"state": "enabled",
5769
"exceptions": [],
5870
"settings": $settingsJson
@@ -64,8 +76,12 @@ class PasswordImporterCssScriptLoader @Inject constructor(
6476
""".trimMargin()
6577
}
6678

67-
private suspend fun loadSettingsJson(): String {
68-
return configStore.getConfig().javascriptConfigGooglePasswords
79+
private suspend fun loadSettingsJsonPassword(): String {
80+
return passwordConfigStore.getConfig().javascriptConfigGooglePasswords
81+
}
82+
83+
private suspend fun loadSettingsJsonBookmark(): String {
84+
return bookmarkConfigStore.getConfig().javascriptConfigGoogleTakeout
6985
}
7086

7187
private fun getUserPreferencesJson(): String {
@@ -84,7 +100,7 @@ class PasswordImporterCssScriptLoader @Inject constructor(
84100

85101
private fun getContentScopeJS(): String {
86102
if (!this::contentScopeJS.isInitialized) {
87-
contentScopeJS = loadJs("autofillPasswordImport.js")
103+
contentScopeJS = loadJs("autofillImport.js")
88104
}
89105
return contentScopeJS
90106
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowFragment.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import androidx.lifecycle.flowWithLifecycle
3333
import androidx.lifecycle.lifecycleScope
3434
import androidx.webkit.WebViewCompat
3535
import com.duckduckgo.anvil.annotations.InjectWith
36-
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
3736
import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin
3837
import com.duckduckgo.autofill.api.BrowserAutofill
3938
import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory
@@ -99,9 +98,6 @@ class ImportGooglePasswordsWebFlowFragment :
9998
@Inject
10099
lateinit var viewModelFactory: FragmentViewModelFactory
101100

102-
@Inject
103-
lateinit var autofillCapabilityChecker: AutofillCapabilityChecker
104-
105101
@Inject
106102
lateinit var credentialAutofillDialogFactory: CredentialAutofillDialogFactory
107103

@@ -115,7 +111,7 @@ class ImportGooglePasswordsWebFlowFragment :
115111
lateinit var passwordBlobConsumer: GooglePasswordBlobConsumer
116112

117113
@Inject
118-
lateinit var passwordImporterScriptLoader: PasswordImporterScriptLoader
114+
lateinit var googleImporterScriptLoader: GoogleImporterScriptLoader
119115

120116
@Inject
121117
lateinit var browserAutofillConfigurator: InternalBrowserAutofillConfigurator
@@ -343,7 +339,7 @@ class ImportGooglePasswordsWebFlowFragment :
343339
@SuppressLint("RequiresFeature", "AddDocumentStartJavaScriptUsage")
344340
private suspend fun configurePasswordImportJavascript(webView: WebView) {
345341
if (importPasswordConfig.getConfig().canInjectJavascript) {
346-
val script = passwordImporterScriptLoader.getScript()
342+
val script = googleImporterScriptLoader.getScriptForPasswordImport()
347343
WebViewCompat.addDocumentStartJavaScript(webView, script, setOf("*"))
348344
}
349345
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright (c) 2025 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.autofill.impl.importing.takeout.store
18+
19+
import com.duckduckgo.autofill.api.AutofillFeature
20+
import com.duckduckgo.common.utils.DispatcherProvider
21+
import com.duckduckgo.di.scopes.AppScope
22+
import com.squareup.anvil.annotations.ContributesBinding
23+
import com.squareup.moshi.JsonAdapter
24+
import com.squareup.moshi.Moshi
25+
import javax.inject.Inject
26+
import kotlinx.coroutines.withContext
27+
import org.json.JSONObject
28+
29+
interface BookmarkImportConfigStore {
30+
suspend fun getConfig(): BookmarkImportSettings
31+
}
32+
33+
data class BookmarkImportSettings(
34+
val canImportFromGoogleTakeout: Boolean,
35+
val launchUrlGoogleTakeout: String,
36+
val canInjectJavascript: Boolean,
37+
val javascriptConfigGoogleTakeout: String,
38+
)
39+
40+
@ContributesBinding(AppScope::class)
41+
class BookmarkImportConfigStoreImpl @Inject constructor(
42+
private val autofillFeature: AutofillFeature,
43+
private val dispatchers: DispatcherProvider,
44+
private val moshi: Moshi,
45+
) : BookmarkImportConfigStore {
46+
47+
private val jsonAdapter: JsonAdapter<ImportConfigJson> by lazy {
48+
moshi.adapter(ImportConfigJson::class.java)
49+
}
50+
51+
override suspend fun getConfig(): BookmarkImportSettings {
52+
return withContext(dispatchers.io()) {
53+
val config = autofillFeature.canImportBookmarksFromGoogleTakeout().getSettings()?.let {
54+
runCatching {
55+
jsonAdapter.fromJson(it)
56+
}.getOrNull()
57+
}
58+
59+
BookmarkImportSettings(
60+
canImportFromGoogleTakeout = autofillFeature.canImportBookmarksFromGoogleTakeout().isEnabled(),
61+
launchUrlGoogleTakeout = config?.launchUrl ?: LAUNCH_URL_DEFAULT,
62+
canInjectJavascript = config?.canInjectJavascript ?: CAN_INJECT_JAVASCRIPT_DEFAULT,
63+
javascriptConfigGoogleTakeout = config?.javascriptConfig?.toString() ?: JAVASCRIPT_CONFIG_DEFAULT,
64+
)
65+
}
66+
}
67+
68+
companion object {
69+
internal const val JAVASCRIPT_CONFIG_DEFAULT = "\"{}\""
70+
internal const val CAN_INJECT_JAVASCRIPT_DEFAULT = true
71+
72+
internal const val LAUNCH_URL_DEFAULT = "https://takeout.google.com/settings/takeout"
73+
}
74+
75+
private data class ImportConfigJson(
76+
val launchUrl: String? = null,
77+
val canInjectJavascript: Boolean = CAN_INJECT_JAVASCRIPT_DEFAULT,
78+
val javascriptConfig: JSONObject? = null,
79+
)
80+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2025 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.autofill.impl.importing.takeout.webflow
18+
19+
import android.os.Parcelable
20+
import kotlinx.parcelize.Parcelize
21+
22+
sealed interface ImportGoogleBookmarkResult : Parcelable {
23+
24+
@Parcelize
25+
data class Success(val importedCount: Int) : ImportGoogleBookmarkResult
26+
27+
@Parcelize
28+
data class UserCancelled(val stage: String) : ImportGoogleBookmarkResult
29+
30+
@Parcelize
31+
data class Error(val reason: ImportGoogleBookmarksWebFlowViewModel.UserCannotImportReason) : ImportGoogleBookmarkResult
32+
33+
companion object {
34+
const val RESULT_KEY = "importBookmarkResult"
35+
const val RESULT_KEY_DETAILS = "importBookmarkResultDetails"
36+
}
37+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2025 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.autofill.impl.importing.takeout.webflow
18+
19+
import android.content.Intent
20+
import android.os.Bundle
21+
import androidx.fragment.app.commit
22+
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
23+
import com.duckduckgo.anvil.annotations.InjectWith
24+
import com.duckduckgo.autofill.impl.R
25+
import com.duckduckgo.autofill.impl.databinding.ActivityImportGoogleBookmarksWebflowBinding
26+
import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmark.AutofillImportViaGoogleTakeoutScreen
27+
import com.duckduckgo.common.ui.DuckDuckGoActivity
28+
import com.duckduckgo.common.ui.viewbinding.viewBinding
29+
import com.duckduckgo.di.scopes.ActivityScope
30+
import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
31+
32+
@InjectWith(ActivityScope::class)
33+
@ContributeToActivityStarter(AutofillImportViaGoogleTakeoutScreen::class)
34+
class ImportGoogleBookmarksWebFlowActivity : DuckDuckGoActivity() {
35+
36+
val binding: ActivityImportGoogleBookmarksWebflowBinding by viewBinding()
37+
38+
override fun onCreate(savedInstanceState: Bundle?) {
39+
super.onCreate(savedInstanceState)
40+
setContentView(binding.root)
41+
configureResultListeners()
42+
launchImportFragment()
43+
}
44+
45+
private fun launchImportFragment() {
46+
supportFragmentManager.commit {
47+
replace(R.id.fragment_container, ImportGoogleBookmarksWebFlowFragment())
48+
}
49+
}
50+
51+
private fun configureResultListeners() {
52+
supportFragmentManager.setFragmentResultListener(ImportGoogleBookmarkResult.Companion.RESULT_KEY, this) { _, result ->
53+
exitWithResult(result)
54+
}
55+
}
56+
57+
private fun exitWithResult(resultBundle: Bundle) {
58+
setResult(RESULT_OK, Intent().putExtras(resultBundle))
59+
finish()
60+
}
61+
62+
fun exitUserCancelled(stage: String) {
63+
val result = Bundle().apply {
64+
putParcelable(
65+
ImportGoogleBookmarkResult.Companion.RESULT_KEY_DETAILS,
66+
ImportGoogleBookmarkResult.UserCancelled(stage),
67+
)
68+
}
69+
exitWithResult(result)
70+
}
71+
}
72+
73+
object ImportGoogleBookmark {
74+
data object AutofillImportViaGoogleTakeoutScreen : ActivityParams {
75+
private fun readResolve(): Any = AutofillImportViaGoogleTakeoutScreen
76+
}
77+
}

0 commit comments

Comments
 (0)