diff --git a/Task b/Task
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCredentialDialogs.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCredentialDialogs.kt
index 84de7dc67da1..ed6d917dea70 100644
--- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCredentialDialogs.kt
+++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCredentialDialogs.kt
@@ -16,7 +16,9 @@
package com.duckduckgo.autofill.api
+import android.os.Bundle
import android.os.Parcelable
+import androidx.core.os.BundleCompat
import androidx.fragment.app.DialogFragment
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.api.domain.app.LoginTriggerType
@@ -184,6 +186,50 @@ interface EmailProtectionInContextSignUpDialog {
}
}
+/**
+ * Dialog which prompts the user to import bookmarks from Google
+ * Results should be handled by defining a fragmentResultListener with key [ImportBookmarksPreImportDialog.RESULT_KEY]
+ * e.g.,
+ * supportFragmentManager.setFragmentResultListener(ImportBookmarksPreImportDialog.RESULT_KEY, this) { _, result ->
+ * val choice = ImportBookmarksPreImportDialog.extractResult(result)
+ * }
+ */
+interface ImportBookmarksPreImportDialog {
+
+ /**
+ * Result of the dialog, as determined by which button the user pressed or if they cancelled the dialog
+ */
+ @Parcelize
+ sealed interface ImportBookmarksPreImportResult : Parcelable {
+
+ /**
+ * User chose to proceed with the Google import
+ */
+ @Parcelize
+ data object ImportBookmarks : ImportBookmarksPreImportResult
+
+ /**
+ * User chose to select a bookmarks file
+ */
+ @Parcelize
+ data object SelectBookmarksFile : ImportBookmarksPreImportResult
+
+ /**
+ * User cancelled the dialog
+ */
+ @Parcelize
+ data object Cancel : ImportBookmarksPreImportResult
+ }
+
+ companion object {
+ const val RESULT_KEY = "ImportBookmarksPreImportDialog"
+
+ fun extractResult(bundle: Bundle): ImportBookmarksPreImportResult? {
+ return BundleCompat.getParcelable(bundle, "result", ImportBookmarksPreImportResult::class.java)
+ }
+ }
+}
+
/**
* Factory used to get instances of the various autofill dialogs
*/
@@ -258,6 +304,11 @@ interface CredentialAutofillDialogFactory {
tabId: String,
url: String,
): DialogFragment
+
+ /**
+ * Creates a dialog which prompts the user to import bookmarks from Google
+ */
+ fun autofillImportBookmarksPreImportDialog(importSource: AutofillImportBookmarksLaunchSource): DialogFragment
}
private fun prefix(
@@ -280,4 +331,5 @@ enum class AutofillImportLaunchSource(val value: String) : Parcelable {
enum class AutofillImportBookmarksLaunchSource(val value: String) : Parcelable {
Unknown("unknown"),
AutofillDevSettings("autofill_dev_settings"),
+ BookmarksScreen("bookmarks_screen"),
}
diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillScreens.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillScreens.kt
index 37e6470b8c34..2c83993011c5 100644
--- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillScreens.kt
+++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillScreens.kt
@@ -54,6 +54,12 @@ sealed interface AutofillScreens {
val loginCredentials: LoginCredentials,
val source: AutofillScreenLaunchSource,
) : ActivityParams
+
+ /**
+ * Launch the Google Bookmarks import flow
+ * @param importSource is used to indicate from where in the app the import was launched
+ */
+ data class AutofillImportViaGoogleTakeoutScreen(val importSource: AutofillImportBookmarksLaunchSource) : ActivityParams
}
enum class AutofillScreenLaunchSource {
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowActivity.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowActivity.kt
index bb9aa8118aff..d7761a0626d8 100644
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowActivity.kt
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowActivity.kt
@@ -29,9 +29,9 @@ import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.appbuildconfig.api.isInternalBuild
import com.duckduckgo.autofill.api.AutofillImportBookmarksLaunchSource
+import com.duckduckgo.autofill.api.AutofillScreens
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.databinding.ActivityImportGoogleBookmarksWebflowBinding
-import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmark.AutofillImportViaGoogleTakeoutScreen
import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmark.AutofillImportViaGoogleTakeoutScreenResultError
import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmark.AutofillImportViaGoogleTakeoutScreenResultSuccess
import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmark.ImportViaGoogleTakeoutScreen
@@ -46,7 +46,7 @@ import logcat.logcat
import javax.inject.Inject
@InjectWith(ActivityScope::class)
-@ContributeToActivityStarter(AutofillImportViaGoogleTakeoutScreen::class)
+@ContributeToActivityStarter(AutofillScreens.AutofillImportViaGoogleTakeoutScreen::class)
@ContributeToActivityStarter(AutofillImportViaGoogleTakeoutScreenResultSuccess::class)
@ContributeToActivityStarter(AutofillImportViaGoogleTakeoutScreenResultError::class)
class ImportGoogleBookmarksWebFlowActivity :
@@ -58,13 +58,13 @@ class ImportGoogleBookmarksWebFlowActivity :
val binding: ActivityImportGoogleBookmarksWebflowBinding by viewBinding()
private var isOverlayCurrentlyShown = false
+ private var isOnResultScreen = false
private val canShowDebugMenu: Boolean
get() = appBuildConfig.isInternalBuild() && isOverlayCurrentlyShown
private val launchSource: AutofillImportBookmarksLaunchSource by lazy {
- intent.getActivityParams(ImportViaGoogleTakeoutScreen::class.java)?.launchSource
- ?: AutofillImportBookmarksLaunchSource.Unknown
+ intent.getActivityParams(ImportViaGoogleTakeoutScreen::class.java)?.launchSource ?: AutofillImportBookmarksLaunchSource.Unknown
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -78,15 +78,23 @@ class ImportGoogleBookmarksWebFlowActivity :
val errorResult = intent.getActivityParams(AutofillImportViaGoogleTakeoutScreenResultError::class.java)
when {
- successResult != null -> showSuccessFragment(successResult.bookmarkCount)
- errorResult != null -> showErrorFragment(errorResult.errorReason)
+ successResult != null -> {
+ isOnResultScreen = true
+ showSuccessFragment(successResult.bookmarkCount)
+ }
+ errorResult != null -> {
+ isOnResultScreen = true
+ showErrorFragment(errorResult.errorReason)
+ }
else -> launchWebFlow()
}
}
private fun launchWebFlow() {
logcat { "Bookmark-import: Starting webflow" }
+ isOnResultScreen = false
replaceFragment(ImportGoogleBookmarksWebFlowFragment())
+ updateToolbarTitle()
invalidateOptionsMenu()
}
@@ -110,6 +118,8 @@ class ImportGoogleBookmarksWebFlowActivity :
replaceFragment(successFragment)
isOverlayCurrentlyShown = false
+ isOnResultScreen = true
+ updateToolbarTitle()
invalidateOptionsMenu()
}
@@ -125,6 +135,8 @@ class ImportGoogleBookmarksWebFlowActivity :
replaceFragment(errorFragment)
isOverlayCurrentlyShown = false
+ isOnResultScreen = true
+ updateToolbarTitle()
invalidateOptionsMenu()
}
@@ -147,6 +159,7 @@ class ImportGoogleBookmarksWebFlowActivity :
private fun showProgressOverlay() {
isOverlayCurrentlyShown = true
+ updateToolbarTitle()
val progressFragment = supportFragmentManager.findFragmentByTag(PROGRESS_OVERLAY_TAG)
if (progressFragment == null) {
@@ -159,6 +172,7 @@ class ImportGoogleBookmarksWebFlowActivity :
private fun hideProgressOverlay() {
isOverlayCurrentlyShown = false
+ updateToolbarTitle()
val progressFragment = supportFragmentManager.findFragmentByTag(PROGRESS_OVERLAY_TAG)
if (progressFragment != null) {
@@ -249,6 +263,7 @@ class ImportGoogleBookmarksWebFlowActivity :
toggleOverlay()
true
}
+
else -> super.onOptionsItemSelected(item)
}
}
@@ -268,7 +283,16 @@ class ImportGoogleBookmarksWebFlowActivity :
setNavigationIcon(com.duckduckgo.mobile.android.R.drawable.ic_close_24)
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
- setTitle("")
+ updateToolbarTitle()
+ }
+
+ private fun updateToolbarTitle() {
+ val title = if (isOverlayCurrentlyShown || isOnResultScreen) {
+ ""
+ } else {
+ getString(R.string.importBookmarksFromGoogleWebFlowTitle)
+ }
+ setTitle(title)
}
companion object {
@@ -283,11 +307,6 @@ object ImportGoogleBookmark {
) : ActivityParams,
Parcelable
- @Parcelize
- data class AutofillImportViaGoogleTakeoutScreen(
- private val source: AutofillImportBookmarksLaunchSource,
- ) : ImportViaGoogleTakeoutScreen(source)
-
@Parcelize
data class AutofillImportViaGoogleTakeoutScreenResultSuccess(
private val source: AutofillImportBookmarksLaunchSource,
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/CredentialAutofillDialogAndroidFactory.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/CredentialAutofillDialogAndroidFactory.kt
index 099f144be784..aeeba2ea092f 100644
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/CredentialAutofillDialogAndroidFactory.kt
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/CredentialAutofillDialogAndroidFactory.kt
@@ -17,6 +17,7 @@
package com.duckduckgo.autofill.impl.ui
import androidx.fragment.app.DialogFragment
+import com.duckduckgo.autofill.api.AutofillImportBookmarksLaunchSource
import com.duckduckgo.autofill.api.AutofillImportLaunchSource
import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory
import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType
@@ -24,6 +25,7 @@ import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.api.domain.app.LoginTriggerType
import com.duckduckgo.autofill.impl.email.EmailProtectionChooseEmailFragment
import com.duckduckgo.autofill.impl.email.incontext.prompt.EmailProtectionInContextSignUpPromptFragment
+import com.duckduckgo.autofill.impl.ui.credential.management.importbookmark.google.preimport.ImportFromGoogleBookmarksPreImportDialog
import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialog
import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.AutofillUseGeneratedPasswordDialogFragment
import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment
@@ -107,4 +109,8 @@ class CredentialAutofillDialogAndroidFactory @Inject constructor() : CredentialA
override fun autofillImportPasswordsPromoDialog(importSource: AutofillImportLaunchSource, tabId: String, url: String): DialogFragment {
return ImportFromGooglePasswordsDialog.instance(importSource = importSource, tabId = tabId, originalUrl = url)
}
+
+ override fun autofillImportBookmarksPreImportDialog(importSource: AutofillImportBookmarksLaunchSource): DialogFragment {
+ return ImportFromGoogleBookmarksPreImportDialog.instance(importSource = importSource)
+ }
}
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importbookmark/google/preimport/ImportFromGoogleBookmarksPreImportDialog.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importbookmark/google/preimport/ImportFromGoogleBookmarksPreImportDialog.kt
index 7f0aacef86b6..9210b1558d5e 100644
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importbookmark/google/preimport/ImportFromGoogleBookmarksPreImportDialog.kt
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importbookmark/google/preimport/ImportFromGoogleBookmarksPreImportDialog.kt
@@ -23,13 +23,15 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.BundleCompat
+import androidx.fragment.app.setFragmentResult
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.autofill.api.AutofillImportBookmarksLaunchSource
import com.duckduckgo.autofill.api.AutofillImportBookmarksLaunchSource.Unknown
+import com.duckduckgo.autofill.api.ImportBookmarksPreImportDialog
+import com.duckduckgo.autofill.api.ImportBookmarksPreImportDialog.ImportBookmarksPreImportResult
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.databinding.ContentImportBookmarksFromGooglePreimportDialogBinding
import com.duckduckgo.autofill.impl.ui.credential.dialog.animateClosed
-import com.duckduckgo.autofill.impl.ui.credential.management.importbookmark.google.preimport.ImportFromGoogleBookmarksPreImportDialog.ImportBookmarksDialog.Companion.KEY_TAB_ID
import com.duckduckgo.common.utils.FragmentViewModelFactory
import com.duckduckgo.di.scopes.FragmentScope
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -57,12 +59,6 @@ class ImportFromGoogleBookmarksPreImportDialog : BottomSheetDialogFragment() {
@Inject
lateinit var viewModelFactory: FragmentViewModelFactory
- private var importClickedCallback: (() -> Unit)? = null
-
- fun setImportClickedCallback(callback: () -> Unit) {
- importClickedCallback = callback
- }
-
private fun getLaunchSource() =
BundleCompat.getParcelable(arguments ?: Bundle(), KEY_LAUNCH_SOURCE, AutofillImportBookmarksLaunchSource::class.java) ?: Unknown
@@ -104,10 +100,31 @@ class ImportFromGoogleBookmarksPreImportDialog : BottomSheetDialogFragment() {
with(binding.importButton) {
setOnClickListener { onImportButtonClicked() }
}
+
+ with(binding.selectFileButton) {
+ setOnClickListener { onSelectFileButtonClicked() }
+ }
}
private fun onImportButtonClicked() {
- importClickedCallback?.invoke()
+ ignoreCancellationEvents = true
+
+ // Send fragment result
+ val result = Bundle().apply {
+ putParcelable(KEY_RESULT, ImportBookmarksPreImportResult.ImportBookmarks)
+ putParcelable(KEY_IMPORT_SOURCE, getLaunchSource())
+ }
+ setFragmentResult(ImportBookmarksPreImportDialog.RESULT_KEY, result)
+ }
+
+ private fun onSelectFileButtonClicked() {
+ ignoreCancellationEvents = true
+
+ val result = Bundle().apply {
+ putParcelable(KEY_RESULT, ImportBookmarksPreImportResult.SelectBookmarksFile)
+ putParcelable(KEY_IMPORT_SOURCE, getLaunchSource())
+ }
+ setFragmentResult(ImportBookmarksPreImportDialog.RESULT_KEY, result)
}
override fun onCancel(dialog: DialogInterface) {
@@ -115,32 +132,42 @@ class ImportFromGoogleBookmarksPreImportDialog : BottomSheetDialogFragment() {
logcat(VERBOSE) { "onCancel: Ignoring cancellation event" }
return
}
+
+ val result = Bundle().apply {
+ putParcelable(KEY_RESULT, ImportBookmarksPreImportResult.Cancel)
+ putParcelable(KEY_IMPORT_SOURCE, getLaunchSource())
+ }
+ setFragmentResult(ImportBookmarksPreImportDialog.RESULT_KEY, result)
}
private fun configureCloseButton(binding: ContentImportBookmarksFromGooglePreimportDialogBinding) {
- binding.closeButton.setOnClickListener { (dialog as BottomSheetDialog).animateClosed() }
+ binding.closeButton.setOnClickListener {
+ ignoreCancellationEvents = true
+
+ val result = Bundle().apply {
+ putParcelable(KEY_RESULT, ImportBookmarksPreImportResult.Cancel)
+ putParcelable(KEY_IMPORT_SOURCE, getLaunchSource())
+ }
+ setFragmentResult(ImportBookmarksPreImportDialog.RESULT_KEY, result)
+
+ (dialog as BottomSheetDialog).animateClosed()
+ }
}
companion object {
private const val KEY_LAUNCH_SOURCE = "launchSource"
+ private const val KEY_IMPORT_SOURCE = "importSource"
+ private const val KEY_RESULT = "result"
fun instance(
importSource: AutofillImportBookmarksLaunchSource,
- tabId: String? = null,
): ImportFromGoogleBookmarksPreImportDialog {
val fragment = ImportFromGoogleBookmarksPreImportDialog()
fragment.arguments =
Bundle().apply {
putParcelable(KEY_LAUNCH_SOURCE, importSource)
- putString(KEY_TAB_ID, tabId)
}
return fragment
}
}
-
- interface ImportBookmarksDialog {
- companion object {
- const val KEY_TAB_ID = "tabId"
- }
- }
}
diff --git a/autofill/autofill-impl/src/main/res/layout/content_import_bookmarks_from_google_preimport_dialog.xml b/autofill/autofill-impl/src/main/res/layout/content_import_bookmarks_from_google_preimport_dialog.xml
index eafe18fe2283..9e2af7ce57f7 100644
--- a/autofill/autofill-impl/src/main/res/layout/content_import_bookmarks_from_google_preimport_dialog.xml
+++ b/autofill/autofill-impl/src/main/res/layout/content_import_bookmarks_from_google_preimport_dialog.xml
@@ -79,14 +79,25 @@
+
+
+ android:text="@string/importBookmarksFromGooglePreImportDialogSelectFileButton"
+ app:daxButtonSize="large"
+ app:layout_constraintEnd_toEndOf="@id/guidelineEnd"
+ app:layout_constraintStart_toStartOf="@id/guidelineStart"
+ app:layout_constraintTop_toBottomOf="@id/importButton" />
diff --git a/autofill/autofill-impl/src/main/res/values/donottranslate.xml b/autofill/autofill-impl/src/main/res/values/donottranslate.xml
index 43358d1b225a..dbc5b1e98b1d 100644
--- a/autofill/autofill-impl/src/main/res/values/donottranslate.xml
+++ b/autofill/autofill-impl/src/main/res/values/donottranslate.xml
@@ -23,7 +23,8 @@
Import Your Bookmarks
Import Bookmarks From Google
Google may ask you to sign in or enter your password to confirm.
- Import Now
+ Import From Google
+ Select Bookmarks Fileā¦
@string/importToDuckDuckGo
Your bookmarks are being imported. This may take a few moments.
diff --git a/autofill/autofill-internal/src/main/java/com/duckduckgo/autofill/internal/AutofillInternalSettingsActivity.kt b/autofill/autofill-internal/src/main/java/com/duckduckgo/autofill/internal/AutofillInternalSettingsActivity.kt
index bd094514f72d..199b12928774 100644
--- a/autofill/autofill-internal/src/main/java/com/duckduckgo/autofill/internal/AutofillInternalSettingsActivity.kt
+++ b/autofill/autofill-internal/src/main/java/com/duckduckgo/autofill/internal/AutofillInternalSettingsActivity.kt
@@ -19,6 +19,7 @@ package com.duckduckgo.autofill.internal
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
+import android.net.Uri
import android.os.Bundle
import android.webkit.CookieManager
import android.widget.Toast
@@ -33,7 +34,10 @@ import com.duckduckgo.app.tabs.BrowserNav
import com.duckduckgo.autofill.api.AutofillFeature
import com.duckduckgo.autofill.api.AutofillImportBookmarksLaunchSource.AutofillDevSettings
import com.duckduckgo.autofill.api.AutofillScreenLaunchSource.InternalDevSettings
+import com.duckduckgo.autofill.api.AutofillScreens.AutofillImportViaGoogleTakeoutScreen
import com.duckduckgo.autofill.api.AutofillScreens.AutofillPasswordsManagementScreen
+import com.duckduckgo.autofill.api.ImportBookmarksPreImportDialog
+import com.duckduckgo.autofill.api.ImportBookmarksPreImportDialog.ImportBookmarksPreImportResult
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.impl.configuration.AutofillJavascriptEnvironmentConfiguration
@@ -54,7 +58,6 @@ import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordRe
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult.Success
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult.UserCancelled
import com.duckduckgo.autofill.impl.importing.takeout.processor.TakeoutBookmarkImporter
-import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmark.AutofillImportViaGoogleTakeoutScreen
import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmark.AutofillImportViaGoogleTakeoutScreenResultError
import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmark.AutofillImportViaGoogleTakeoutScreenResultSuccess
import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmarkResult
@@ -225,6 +228,19 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
}
}
+ private val importBookmarksFileLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK) {
+ val selectedFile = result.data?.data
+ if (selectedFile != null) {
+ // Dismiss dialog since user successfully selected a file
+ hidePreImportDialog()
+ importBookmarksFromFile(selectedFile)
+ }
+ }
+ // Note: If resultCode != RESULT_OK, user cancelled file picker - dialog stays open
+ }
+
private suspend fun onBookmarksExtracted(extractionResult: ExtractionResult.Success) {
when (
val importResult =
@@ -395,6 +411,7 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
configureDeclineCounterHandlers()
configureImportPasswordsEventHandlers()
configureImportBookmarksEventHandlers()
+ setupImportBookmarksResultListener()
}
private fun configureReportBreakagesHandlers() {
@@ -418,13 +435,6 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
if (importGooglePasswordsCapabilityChecker.webViewCapableOfImporting()) {
try {
val dialog = ImportFromGoogleBookmarksPreImportDialog.instance(AutofillDevSettings)
- dialog.setImportClickedCallback {
- val intent = globalActivityStarter.startIntent(
- this@AutofillInternalSettingsActivity,
- AutofillImportViaGoogleTakeoutScreen(AutofillDevSettings),
- )
- importGoogleBookmarksFlowLauncher.launch(intent)
- }
dialog.show(supportFragmentManager, TAG_PRE_IMPORT_BOOKMARKS)
} catch (e: Exception) {
val message = "Error launching bookmark import flow: ${e.message}"
@@ -860,6 +870,64 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
Toast.makeText(this, R.string.autofillDevSettingsGoogleLogoutSuccess, Toast.LENGTH_SHORT).show()
}
+ private fun launchBookmarkImportChooseFile() {
+ val intent = Intent()
+ .setType("text/html")
+ .setAction(Intent.ACTION_GET_CONTENT)
+
+ importBookmarksFileLauncher.launch(
+ Intent.createChooser(intent, "Select Bookmarks File"),
+ )
+ }
+
+ private fun setupImportBookmarksResultListener() {
+ supportFragmentManager.setFragmentResultListener(ImportBookmarksPreImportDialog.RESULT_KEY, this) { _, bundle ->
+ val result = ImportBookmarksPreImportDialog.extractResult(bundle)
+
+ when (result) {
+ ImportBookmarksPreImportResult.ImportBookmarks -> {
+ val intent = globalActivityStarter.startIntent(
+ this@AutofillInternalSettingsActivity,
+ AutofillImportViaGoogleTakeoutScreen(AutofillDevSettings),
+ )
+ importGoogleBookmarksFlowLauncher.launch(intent)
+ }
+ ImportBookmarksPreImportResult.SelectBookmarksFile -> launchBookmarkImportChooseFile()
+ ImportBookmarksPreImportResult.Cancel, null -> hidePreImportDialog()
+ }
+ }
+ }
+
+ private fun importBookmarksFromFile(uri: Uri) {
+ lifecycleScope.launch(dispatchers.io()) {
+ try {
+ val result = takeoutBookmarkImporter.importBookmarks(
+ uri,
+ ImportFolder.Folder("Imported Bookmarks"),
+ )
+
+ withContext(dispatchers.main()) {
+ when (result) {
+ is ImportSavedSitesResult.Success -> {
+ val message = "Successfully imported ${result.savedSites.size} bookmarks"
+ Toast.makeText(this@AutofillInternalSettingsActivity, message, Toast.LENGTH_LONG).show()
+ }
+ is ImportSavedSitesResult.Error -> {
+ val message = "Failed to import bookmarks: ${result.exception.message}"
+ Toast.makeText(this@AutofillInternalSettingsActivity, message, Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+ } catch (e: Exception) {
+ withContext(dispatchers.main()) {
+ val message = "Error importing bookmarks: ${e.message}"
+ Toast.makeText(this@AutofillInternalSettingsActivity, message, Toast.LENGTH_LONG).show()
+ logcat { "Error importing bookmarks from file: ${e.message}" }
+ }
+ }
+ }
+ }
+
companion object {
fun intent(context: Context): Intent = Intent(context, AutofillInternalSettingsActivity::class.java)
diff --git a/saved-sites/saved-sites-impl/build.gradle b/saved-sites/saved-sites-impl/build.gradle
index fc8cb1d4d8da..2754f3d44ada 100644
--- a/saved-sites/saved-sites-impl/build.gradle
+++ b/saved-sites/saved-sites-impl/build.gradle
@@ -37,6 +37,7 @@ dependencies {
implementation project(path: ':navigation-api')
implementation project(path: ':new-tab-page-api')
implementation project(path: ':data-store-api')
+ implementation project(path: ':autofill-api')
implementation project(path: ':saved-sites-store')
anvil project(path: ':anvil-compiler')
diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksActivity.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksActivity.kt
index 4191a8c2296e..a9ac055d6c27 100644
--- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksActivity.kt
+++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksActivity.kt
@@ -31,6 +31,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.children
import androidx.core.view.isVisible
+import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.SimpleItemAnimator
@@ -38,6 +39,10 @@ import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.browser.favicon.FaviconManager
import com.duckduckgo.app.tabs.BrowserNav
+import com.duckduckgo.autofill.api.AutofillImportBookmarksLaunchSource
+import com.duckduckgo.autofill.api.AutofillScreens.AutofillImportViaGoogleTakeoutScreen
+import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory
+import com.duckduckgo.autofill.api.ImportBookmarksPreImportDialog
import com.duckduckgo.browser.api.ui.BrowserScreens.BookmarksScreenNoParams
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.menu.PopupMenu
@@ -72,11 +77,13 @@ import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.Expor
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.ImportedSavedSites
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.LaunchAddFolder
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.LaunchBookmarkExport
-import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.LaunchBookmarkImport
+import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.LaunchBookmarkImportFile
+import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.LaunchBookmarkImportTakeoutFlow
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.LaunchSyncSettings
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.OpenBookmarkFolder
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.OpenSavedSite
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.ReevalutePromotions
+import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.ShowBookmarkImportDialog
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.ShowBrowserMenu
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.ShowEditBookmarkFolder
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.ShowEditSavedSite
@@ -99,6 +106,7 @@ import java.util.Date
import java.util.Locale
import java.util.TimeZone
import javax.inject.Inject
+import com.duckduckgo.autofill.api.ImportBookmarksPreImportDialog.ImportBookmarksPreImportResult as ImportResult
import com.duckduckgo.mobile.android.R as commonR
@InjectWith(ActivityScope::class)
@@ -123,6 +131,9 @@ class BookmarksActivity : DuckDuckGoActivity(), BookmarksScreenPromotionPlugin.C
@Inject
lateinit var bookmarksSortingFeature: BookmarksSortingFeature
+ @Inject
+ lateinit var credentialAutofillDialogFactory: CredentialAutofillDialogFactory
+
private lateinit var bookmarksAdapter: BookmarksAdapter
private lateinit var searchListener: BookmarksQueryListener
@@ -167,6 +178,11 @@ class BookmarksActivity : DuckDuckGoActivity(), BookmarksScreenPromotionPlugin.C
viewModel.userReturnedFromSyncSettings()
}
+ private val importGoogleBookmarksFlowLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ handleBookmarkImportResult(result.resultCode)
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
contentBookmarksBinding = ContentBookmarksBinding.bind(binding.root)
@@ -177,6 +193,7 @@ class BookmarksActivity : DuckDuckGoActivity(), BookmarksScreenPromotionPlugin.C
observeViewModel()
observeItemsToDisplay()
initializeSearchBar()
+ setupImportBookmarksResultListener()
viewModel.fetchBookmarksAndFolders(getParentFolderId())
}
@@ -266,9 +283,12 @@ class BookmarksActivity : DuckDuckGoActivity(), BookmarksScreenPromotionPlugin.C
if (resultCode == RESULT_OK) {
val selectedFile = data?.data
if (selectedFile != null) {
+ // Dismiss dialog since user successfully selected a file
+ dismissImportBookmarksDialog()
viewModel.importBookmarks(selectedFile)
}
}
+ // Note: If resultCode != RESULT_OK, user cancelled file picker - dialog stays open
}
EXPORT_BOOKMARKS_REQUEST_CODE -> {
@@ -389,13 +409,55 @@ class BookmarksActivity : DuckDuckGoActivity(), BookmarksScreenPromotionPlugin.C
is LaunchSyncSettings -> launchSyncSettings()
is ReevalutePromotions -> configurePromotionsContainer()
is ShowBrowserMenu -> showBookmarksPopupMenu(it.buttonsDisabled, it.sortingMode)
- is LaunchBookmarkImport -> launchBookmarkImport()
+ is LaunchBookmarkImportFile -> launchBookmarkImportChooseFile()
+ is LaunchBookmarkImportTakeoutFlow -> launchBookmarkImportTakeoutFlow()
+ is ShowBookmarkImportDialog -> showBookmarkImportDialog()
is LaunchBookmarkExport -> launchBookmarkExport()
is LaunchAddFolder -> launchAddFolder()
}
}
}
+ private fun setupImportBookmarksResultListener() {
+ supportFragmentManager.setFragmentResultListener(ImportBookmarksPreImportDialog.RESULT_KEY, this) { _, bundle ->
+ val result = ImportBookmarksPreImportDialog.extractResult(bundle)
+
+ when (result) {
+ ImportResult.ImportBookmarks -> launchBookmarkImportTakeoutFlow()
+ ImportResult.SelectBookmarksFile -> launchBookmarkImportChooseFile()
+ ImportResult.Cancel, null -> {
+ // User explicitly cancelled - dismiss dialog
+ dismissImportBookmarksDialog()
+ }
+ }
+ }
+ }
+
+ private fun showBookmarkImportDialog() {
+ val dialog = credentialAutofillDialogFactory.autofillImportBookmarksPreImportDialog(
+ importSource = AutofillImportBookmarksLaunchSource.BookmarksScreen,
+ )
+ dialog.show(supportFragmentManager, DIALOG_TAG_IMPORT_BOOKMARKS)
+ }
+
+ private fun handleBookmarkImportResult(resultCode: Int) {
+ when (resultCode) {
+ RESULT_OK -> dismissImportBookmarksDialog()
+ else -> {
+ // Flow was cancelled or failed - keep dialog open so user can try again
+ }
+ }
+ }
+
+ private fun dismissImportBookmarksDialog() {
+ val dialog = supportFragmentManager.findFragmentByTag(DIALOG_TAG_IMPORT_BOOKMARKS)
+ dialog?.let {
+ if (it is DialogFragment) {
+ it.dismiss()
+ }
+ }
+ }
+
private fun observeItemsToDisplay() {
viewModel.itemsToDisplay.onEach { items ->
bookmarksAdapter.setItems(
@@ -425,7 +487,14 @@ class BookmarksActivity : DuckDuckGoActivity(), BookmarksScreenPromotionPlugin.C
faviconPrompt.show()
}
- private fun launchBookmarkImport() {
+ private fun launchBookmarkImportTakeoutFlow() {
+ val intent = globalActivityStarter.startIntent(
+ this,
+ AutofillImportViaGoogleTakeoutScreen(AutofillImportBookmarksLaunchSource.BookmarksScreen),
+ )
+ importGoogleBookmarksFlowLauncher.launch(intent)
+ }
+ private fun launchBookmarkImportChooseFile() {
val intent = Intent()
.setType("text/html")
.setAction(Intent.ACTION_GET_CONTENT)
@@ -793,6 +862,10 @@ class BookmarksActivity : DuckDuckGoActivity(), BookmarksScreenPromotionPlugin.C
}
}
+ private fun String.showSnackbar(duration: Int = Snackbar.LENGTH_LONG) {
+ Snackbar.make(binding.root, this, duration).show()
+ }
+
companion object {
const val SAVED_SITE_URL_EXTRA = "SAVED_SITE_URL_EXTRA"
@@ -828,5 +901,7 @@ class BookmarksActivity : DuckDuckGoActivity(), BookmarksScreenPromotionPlugin.C
private val formatter: SimpleDateFormat = SimpleDateFormat("yyyyMMdd", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
+
+ private const val DIALOG_TAG_IMPORT_BOOKMARKS = "ImportBookmarksPreImportDialog"
}
}
diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModel.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModel.kt
index 151f3c99d7b5..32291c6edee6 100644
--- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModel.kt
+++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModel.kt
@@ -23,6 +23,7 @@ import com.duckduckgo.app.browser.favicon.FaviconManager
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
+import com.duckduckgo.autofill.api.AutofillFeature
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.SingleLiveEvent
import com.duckduckgo.di.scopes.ActivityScope
@@ -47,7 +48,7 @@ import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.Expor
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.ImportedSavedSites
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.LaunchAddFolder
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.LaunchBookmarkExport
-import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.LaunchBookmarkImport
+import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.LaunchBookmarkImportFile
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.OpenBookmarkFolder
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.OpenSavedSite
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksViewModel.Command.ShowBrowserMenu
@@ -84,6 +85,7 @@ class BookmarksViewModel @Inject constructor(
private val faviconsFetchingPrompt: FaviconsFetchingPrompt,
private val bookmarksDataStore: BookmarksDataStore,
private val dispatcherProvider: DispatcherProvider,
+ private val autofillFeature: AutofillFeature,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
) : EditSavedSiteListener, AddBookmarkFolderListener, EditBookmarkFolderListener, DeleteBookmarkListener, ViewModel() {
@@ -106,7 +108,9 @@ class BookmarksViewModel @Inject constructor(
class ConfirmDeleteBookmarkFolder(val bookmarkFolder: BookmarkFolder) : Command()
data class ImportedSavedSites(val importSavedSitesResult: ImportSavedSitesResult) : Command()
data class ExportedSavedSites(val exportSavedSitesResult: ExportSavedSitesResult) : Command()
- data object LaunchBookmarkImport : Command()
+ data object ShowBookmarkImportDialog : Command()
+ data object LaunchBookmarkImportFile : Command()
+ data object LaunchBookmarkImportTakeoutFlow : Command()
data object LaunchBookmarkExport : Command()
data object LaunchAddFolder : Command()
data object ShowFaviconsPrompt : Command()
@@ -516,7 +520,14 @@ class BookmarksViewModel @Inject constructor(
fun onImportBookmarksClicked() {
pixel.fire(SavedSitesPixelName.BOOKMARK_MENU_IMPORT_CLICKED)
- command.value = LaunchBookmarkImport
+
+ viewModelScope.launch {
+ command.value = if (autofillFeature.canImportBookmarksFromGoogleTakeout().isEnabled()) {
+ Command.ShowBookmarkImportDialog
+ } else {
+ LaunchBookmarkImportFile
+ }
+ }
}
fun onExportBookmarksClicked() {
diff --git a/saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModelTest.kt b/saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModelTest.kt
index 7868e3f15d83..5890ef6f1610 100644
--- a/saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModelTest.kt
+++ b/saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModelTest.kt
@@ -16,12 +16,16 @@
package com.duckduckgo.savedsites.impl.bookmarks
+import android.annotation.SuppressLint
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import com.duckduckgo.app.browser.favicon.FaviconManager
import com.duckduckgo.app.statistics.pixels.Pixel
+import com.duckduckgo.autofill.api.AutofillFeature
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.common.test.InstantSchedulersRule
+import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
+import com.duckduckgo.feature.toggles.api.Toggle.State
import com.duckduckgo.savedsites.api.SavedSitesRepository
import com.duckduckgo.savedsites.api.models.BookmarkFolder
import com.duckduckgo.savedsites.api.models.BookmarkFolderItem
@@ -50,6 +54,7 @@ import org.junit.Test
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.*
+@SuppressLint("DenyListedApi")
class BookmarksViewModelTest {
@get:Rule
@@ -64,6 +69,8 @@ class BookmarksViewModelTest {
@Suppress("unused")
val coroutineRule = CoroutineTestRule()
+ private val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java)
+
private val commandCaptor = argumentCaptor()
private val viewStateCaptor = argumentCaptor()
@@ -109,6 +116,7 @@ class BookmarksViewModelTest {
faviconsFetchingPrompt,
bookmarksDataStore,
coroutineRule.testDispatcherProvider,
+ autofillFeature,
coroutineRule.testScope,
)
model.viewState.observeForever(viewStateObserver)
@@ -124,6 +132,8 @@ class BookmarksViewModelTest {
whenever(bookmarksDataStore.getSortingMode()).thenReturn(NAME)
testee.fetchBookmarksAndFolders(SavedSitesNames.BOOKMARKS_ROOT)
+
+ autofillFeature.canImportBookmarksFromGoogleTakeout().setRawStoredState(State(true))
}
@After
@@ -591,12 +601,28 @@ class BookmarksViewModelTest {
}
@Test
- fun whenImportBookmarksClickedThenPixelAndCommandSent() {
+ fun whenImportBookmarksClickedThenPixelSent() {
testee.onImportBookmarksClicked()
verify(pixel).fire(SavedSitesPixelName.BOOKMARK_MENU_IMPORT_CLICKED)
+ }
+
+ @Test
+ fun whenImportBookmarksClickedAndFeatureEnabledThenShowDialog() {
+ autofillFeature.canImportBookmarksFromGoogleTakeout().setRawStoredState(State(true))
+ testee.onImportBookmarksClicked()
+
+ verify(commandObserver).onChanged(commandCaptor.capture())
+ assertEquals(BookmarksViewModel.Command.ShowBookmarkImportDialog, commandCaptor.lastValue)
+ }
+
+ @Test
+ fun whenImportBookmarksClickedAndFeatureDisabledThenLaunchFileImport() {
+ autofillFeature.canImportBookmarksFromGoogleTakeout().setRawStoredState(State(false))
+ testee.onImportBookmarksClicked()
+
verify(commandObserver).onChanged(commandCaptor.capture())
- assertEquals(BookmarksViewModel.Command.LaunchBookmarkImport, commandCaptor.lastValue)
+ assertEquals(BookmarksViewModel.Command.LaunchBookmarkImportFile, commandCaptor.lastValue)
}
@Test