diff --git a/autofill/autofill-impl/build.gradle b/autofill/autofill-impl/build.gradle
index 8ccbc43a69fc..bad937238f70 100644
--- a/autofill/autofill-impl/build.gradle
+++ b/autofill/autofill-impl/build.gradle
@@ -72,6 +72,7 @@ dependencies {
implementation AndroidX.biometric
implementation "net.zetetic:android-database-sqlcipher:_"
+ implementation "com.facebook.shimmer:shimmer:_"
// Testing dependencies
testImplementation "org.mockito.kotlin:mockito-kotlin:_"
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/deviceauth/AutofillAuthorizationGracePeriod.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/deviceauth/AutofillAuthorizationGracePeriod.kt
index 90eed5c0f602..27674b62b67e 100644
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/deviceauth/AutofillAuthorizationGracePeriod.kt
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/deviceauth/AutofillAuthorizationGracePeriod.kt
@@ -40,6 +40,16 @@ interface AutofillAuthorizationGracePeriod {
*/
fun recordSuccessfulAuthorization()
+ /**
+ * Requests an extended grace period. This may extend the grace period to a longer duration.
+ */
+ fun requestExtendedGracePeriod()
+
+ /**
+ * Removes the request for an extended grace period
+ */
+ fun removeRequestForExtendedGracePeriod()
+
/**
* Invalidates the grace period, so that the next call to [isAuthRequired] will return true
*/
@@ -53,12 +63,21 @@ class AutofillTimeBasedAuthorizationGracePeriod @Inject constructor(
) : AutofillAuthorizationGracePeriod {
private var lastSuccessfulAuthTime: Long? = null
+ private var extendedGraceTimeRequested: Long? = null
override fun recordSuccessfulAuthorization() {
lastSuccessfulAuthTime = timeProvider.currentTimeMillis()
Timber.v("Recording timestamp of successful auth")
}
+ override fun requestExtendedGracePeriod() {
+ extendedGraceTimeRequested = timeProvider.currentTimeMillis()
+ }
+
+ override fun removeRequestForExtendedGracePeriod() {
+ extendedGraceTimeRequested = null
+ }
+
override fun isAuthRequired(): Boolean {
lastSuccessfulAuthTime?.let { lastAuthTime ->
val timeSinceLastAuth = timeProvider.currentTimeMillis() - lastAuthTime
@@ -67,17 +86,36 @@ class AutofillTimeBasedAuthorizationGracePeriod @Inject constructor(
Timber.v("Within grace period; auth not required")
return false
}
+
+ if (inExtendedGracePeriod()) {
+ Timber.v("Within extended grace period; auth not required")
+ return false
+ }
}
+
+ removeRequestForExtendedGracePeriod()
Timber.v("No last auth time recorded or outside grace period; auth required")
return true
}
+ private fun inExtendedGracePeriod(): Boolean {
+ val extendedRequest = extendedGraceTimeRequested
+ if (extendedRequest == null) {
+ return false
+ } else {
+ val timeSinceExtendedGrace = timeProvider.currentTimeMillis() - extendedRequest
+ return timeSinceExtendedGrace <= AUTH_GRACE_EXTENDED_PERIOD_MS
+ }
+ }
+
override fun invalidate() {
lastSuccessfulAuthTime = null
+ removeRequestForExtendedGracePeriod()
}
companion object {
private const val AUTH_GRACE_PERIOD_MS = 15_000
+ private const val AUTH_GRACE_EXTENDED_PERIOD_MS = 120_000
}
}
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt
index 9b46fa6d9b86..bdc925101e0a 100644
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt
@@ -24,9 +24,7 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENGAGEMENT
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENGAGEMENT_ONBOARDED_USER
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENGAGEMENT_STACKED_LOGINS
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_COPIED_DESKTOP_LINK
-import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_CTA_BUTTON
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_GET_DESKTOP_BROWSER
-import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_OVERFLOW_MENU
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_SHARED_DESKTOP_LINK
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_SYNC_WITH_DESKTOP
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_RESTARTED
@@ -43,6 +41,8 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAK
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAKAGE_REPORT_CONFIRMATION_CONFIRMED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAKAGE_REPORT_CONFIRMATION_DISMISSED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAKAGE_REPORT_CONFIRMATION_DISPLAYED
+import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON
+import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_TOOLTIP_DISMISSED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ADDRESS
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ALIAS
@@ -142,8 +142,8 @@ enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName
AUTOFILL_TOGGLED_ON_SEARCH("m_autofill_toggled_on"),
AUTOFILL_TOGGLED_OFF_SEARCH("m_autofill_toggled_off"),
- AUTOFILL_IMPORT_PASSWORDS_CTA_BUTTON("m_autofill_logins_import_no_passwords"),
- AUTOFILL_IMPORT_PASSWORDS_OVERFLOW_MENU("m_autofill_logins_import"),
+ AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON("m_autofill_logins_import_no_passwords"),
+ AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU("m_autofill_logins_import"),
AUTOFILL_IMPORT_PASSWORDS_GET_DESKTOP_BROWSER("m_autofill_logins_import_get_desktop"),
AUTOFILL_IMPORT_PASSWORDS_SYNC_WITH_DESKTOP("m_autofill_logins_import_sync"),
AUTOFILL_IMPORT_PASSWORDS_USER_TOOK_NO_ACTION("m_autofill_logins_import_no-action"),
@@ -177,8 +177,8 @@ object AutofillPixelsRequiringDataCleaning : PixelParamRemovalPlugin {
AUTOFILL_ENGAGEMENT_ONBOARDED_USER.pixelName to PixelParameter.removeAtb(),
AUTOFILL_ENGAGEMENT_STACKED_LOGINS.pixelName to PixelParameter.removeAtb(),
- AUTOFILL_IMPORT_PASSWORDS_CTA_BUTTON.pixelName to PixelParameter.removeAtb(),
- AUTOFILL_IMPORT_PASSWORDS_OVERFLOW_MENU.pixelName to PixelParameter.removeAtb(),
+ AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON.pixelName to PixelParameter.removeAtb(),
+ AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU.pixelName to PixelParameter.removeAtb(),
AUTOFILL_IMPORT_PASSWORDS_GET_DESKTOP_BROWSER.pixelName to PixelParameter.removeAtb(),
AUTOFILL_IMPORT_PASSWORDS_SYNC_WITH_DESKTOP.pixelName to PixelParameter.removeAtb(),
AUTOFILL_IMPORT_PASSWORDS_USER_TOOK_NO_ACTION.pixelName to PixelParameter.removeAtb(),
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt
index 151aed68cee7..fb9683631aa1 100644
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt
@@ -16,6 +16,7 @@
package com.duckduckgo.autofill.impl.ui.credential.management
+import android.os.Parcelable
import android.util.Patterns
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -36,6 +37,7 @@ import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthConfiguration
+import com.duckduckgo.autofill.impl.importing.gpm.feature.AutofillImportPasswordsFeature
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DELETE_LOGIN
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_DISABLED
@@ -112,6 +114,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesViewModel(ActivityScope::class)
@@ -133,6 +136,7 @@ class AutofillSettingsViewModel @Inject constructor(
private val autofillBreakageReportSender: AutofillBreakageReportSender,
private val autofillBreakageReportDataStore: AutofillSiteBreakageReportingDataStore,
private val autofillBreakageReportCanShowRules: AutofillBreakageReportCanShowRules,
+ private val importPasswordsFeature: AutofillImportPasswordsFeature,
) : ViewModel() {
private val _viewState = MutableStateFlow(ViewState())
@@ -690,7 +694,13 @@ class AutofillSettingsViewModel @Inject constructor(
}
fun onImportPasswords() {
- addCommand(LaunchImportPasswords)
+ viewModelScope.launch(dispatchers.io()) {
+ with(importPasswordsFeature) {
+ val gpmImport = self().isEnabled() && canImportFromGooglePasswordManager().isEnabled()
+ val importConfig = ImportPasswordConfig(canImportFromGooglePasswordManager = gpmImport)
+ addCommand(LaunchImportPasswords(importConfig))
+ }
+ }
}
fun onReportBreakageClicked() {
@@ -702,7 +712,10 @@ class AutofillSettingsViewModel @Inject constructor(
}
}
- fun updateCurrentSite(currentUrl: String?, privacyProtectionEnabled: Boolean?) {
+ fun updateCurrentSite(
+ currentUrl: String?,
+ privacyProtectionEnabled: Boolean?,
+ ) {
val updatedReportBreakageState = _viewState.value.reportBreakageState.copy(
currentUrl = currentUrl,
privacyProtectionEnabled = privacyProtectionEnabled,
@@ -854,12 +867,18 @@ class AutofillSettingsViewModel @Inject constructor(
data object LaunchResetNeverSaveListConfirmation : ListModeCommand()
data class LaunchDeleteAllPasswordsConfirmation(val numberToDelete: Int) : ListModeCommand()
data class PromptUserToAuthenticateMassDeletion(val authConfiguration: AuthConfiguration) : ListModeCommand()
- data object LaunchImportPasswords : ListModeCommand()
+ data class LaunchImportPasswords(val config: ImportPasswordConfig) : ListModeCommand()
+
data class LaunchReportAutofillBreakageConfirmation(val eTldPlusOne: String) : ListModeCommand()
data object ShowUserReportSentMessage : ListModeCommand()
data object ReevalutePromotions : ListModeCommand()
}
+ @Parcelize
+ data class ImportPasswordConfig(
+ val canImportFromGooglePasswordManager: Boolean,
+ ) : Parcelable
+
sealed class DuckAddressStatus {
object NotADuckAddress : DuckAddressStatus()
data class FetchingActivationStatus(val address: String) : DuckAddressStatus()
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt
index 45a54b9a7c6e..7b5fbe61d7e2 100644
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt
@@ -25,11 +25,13 @@ import android.view.ViewGroup
import android.widget.CompoundButton
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.os.BundleCompat
import androidx.core.text.toSpanned
import androidx.core.view.MenuProvider
import androidx.core.view.children
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
+import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Lifecycle.State
import androidx.lifecycle.ViewModelProvider
@@ -47,8 +49,10 @@ import com.duckduckgo.autofill.impl.databinding.FragmentAutofillManagementListMo
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthConfiguration
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthResult.Success
-import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_CTA_BUTTON
-import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_OVERFLOW_MENU
+import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult.Finished
+import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult.InProgress
+import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON
+import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementActivity
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementRecyclerAdapter
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementRecyclerAdapter.ContextMenuAction.CopyPassword
@@ -56,6 +60,7 @@ import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementR
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementRecyclerAdapter.ContextMenuAction.Delete
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementRecyclerAdapter.ContextMenuAction.Edit
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel
+import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ImportPasswordConfig
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.LaunchDeleteAllPasswordsConfirmation
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.LaunchImportPasswords
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.LaunchReportAutofillBreakageConfirmation
@@ -68,6 +73,8 @@ import com.duckduckgo.autofill.impl.ui.credential.management.sorting.CredentialG
import com.duckduckgo.autofill.impl.ui.credential.management.sorting.InitialExtractor
import com.duckduckgo.autofill.impl.ui.credential.management.suggestion.SuggestionListBuilder
import com.duckduckgo.autofill.impl.ui.credential.management.suggestion.SuggestionMatcher
+import com.duckduckgo.autofill.impl.ui.credential.management.viewing.SelectImportPasswordMethodDialog.Companion.Result
+import com.duckduckgo.autofill.impl.ui.credential.management.viewing.SelectImportPasswordMethodDialog.Companion.Result.UserChoseGcmImport
import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
import com.duckduckgo.common.ui.DuckDuckGoFragment
import com.duckduckgo.common.ui.view.SearchBar
@@ -141,7 +148,8 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
private var searchMenuItem: MenuItem? = null
private var resetNeverSavedSitesMenuItem: MenuItem? = null
private var deleteAllPasswordsMenuItem: MenuItem? = null
- private var importPasswordsMenuItem: MenuItem? = null
+ private var syncDesktopPasswordsMenuItem: MenuItem? = null
+ private var importGooglePasswordsMenuItem: MenuItem? = null
private val globalAutofillToggleListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) return@OnCheckedChangeListener
@@ -239,10 +247,36 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
}
private fun configureImportPasswordsButton() {
- binding.emptyStateLayout.importPasswordsButton.setOnClickListener {
+ binding.emptyStateLayout.importPasswordsFromGoogleButton.setOnClickListener {
viewModel.onImportPasswords()
- pixel.fire(AUTOFILL_IMPORT_PASSWORDS_CTA_BUTTON)
}
+
+ binding.emptyStateLayout.importPasswordsViaDesktopSyncButton.setOnClickListener {
+ launchImportPasswordsFromDesktopSyncScreen()
+ pixel.fire(AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON)
+ }
+
+ setFragmentResultListener(SelectImportPasswordMethodDialog.RESULT_KEY) { _, result ->
+ when (val importResult = BundleCompat.getParcelable(result, SelectImportPasswordMethodDialog.RESULT_KEY_DETAILS, Result::class.java)) {
+ is UserChoseGcmImport -> {
+ when (importResult.importResult) {
+ is Finished -> {
+ }
+ is InProgress -> {
+ }
+ }
+ }
+ else -> {}
+ }
+ }
+ }
+
+ private fun userImportedViaCsv(numberImported: Int) {
+ Snackbar.make(binding.root, getString(R.string.autofillImportCsvPasswordsSuccessMessage, numberImported), Snackbar.LENGTH_LONG).show()
+ }
+
+ private fun userImportViaGcm(numberImported: Int) {
+ Snackbar.make(binding.root, getString(R.string.autofillImportGooglePasswordsSuccessMessage, numberImported), Snackbar.LENGTH_LONG).show()
}
private fun configureToolbar() {
@@ -256,7 +290,8 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
searchMenuItem = menu.findItem(R.id.searchLogins)
resetNeverSavedSitesMenuItem = menu.findItem(R.id.resetNeverSavedSites)
deleteAllPasswordsMenuItem = menu.findItem(R.id.deleteAllPasswords)
- importPasswordsMenuItem = menu.findItem(R.id.importPasswords)
+ syncDesktopPasswordsMenuItem = menu.findItem(R.id.syncDesktopPasswords)
+ importGooglePasswordsMenuItem = menu.findItem(R.id.importGooglePasswords)
initializeSearchBar()
}
@@ -266,7 +301,8 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
searchMenuItem?.isVisible = loginsSaved
deleteAllPasswordsMenuItem?.isVisible = loginsSaved
resetNeverSavedSitesMenuItem?.isVisible = viewModel.neverSavedSitesViewState.value.showOptionToReset
- importPasswordsMenuItem?.isVisible = loginsSaved
+ syncDesktopPasswordsMenuItem?.isVisible = loginsSaved
+ importGooglePasswordsMenuItem?.isVisible = loginsSaved
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
@@ -286,9 +322,14 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
true
}
- R.id.importPasswords -> {
+ R.id.importGooglePasswords -> {
viewModel.onImportPasswords()
- pixel.fire(AUTOFILL_IMPORT_PASSWORDS_OVERFLOW_MENU)
+ true
+ }
+
+ R.id.syncDesktopPasswords -> {
+ launchImportPasswordsFromDesktopSyncScreen()
+ pixel.fire(AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU)
true
}
@@ -380,7 +421,7 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
LaunchResetNeverSaveListConfirmation -> launchResetNeverSavedSitesConfirmation()
is LaunchDeleteAllPasswordsConfirmation -> launchDeleteAllLoginsConfirmationDialog(command.numberToDelete)
is PromptUserToAuthenticateMassDeletion -> promptUserToAuthenticateMassDeletion(command.authConfiguration)
- is LaunchImportPasswords -> launchImportPasswordsScreen()
+ is LaunchImportPasswords -> launchImportPasswordsScreen(command.config)
is LaunchReportAutofillBreakageConfirmation -> launchReportBreakageConfirmation(command.eTldPlusOne)
is ShowUserReportSentMessage -> showUserReportSentMessage()
is ReevalutePromotions -> configurePromotionsContainer()
@@ -392,7 +433,19 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
Snackbar.make(binding.root, R.string.autofillManagementReportBreakageSuccessMessage, Snackbar.LENGTH_LONG).show()
}
- private fun launchImportPasswordsScreen() {
+ private fun launchImportPasswordsScreen(config: ImportPasswordConfig) {
+ context?.let {
+ if (!config.canImportFromGooglePasswordManager) {
+ // fallback to existing import screen
+ launchImportPasswordsFromDesktopSyncScreen()
+ } else {
+ val dialog = SelectImportPasswordMethodDialog.instance(config)
+ dialog.show(parentFragmentManager, "SelectImportPasswordMethodDialog")
+ }
+ }
+ }
+
+ private fun launchImportPasswordsFromDesktopSyncScreen() {
context?.let {
globalActivityStarter.start(it, ImportPasswordActivityParams)
}
@@ -603,7 +656,11 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
}
companion object {
- fun instance(currentUrl: String? = null, privacyProtectionEnabled: Boolean?, source: AutofillSettingsLaunchSource? = null) =
+ fun instance(
+ currentUrl: String? = null,
+ privacyProtectionEnabled: Boolean?,
+ source: AutofillSettingsLaunchSource? = null,
+ ) =
AutofillManagementListMode().apply {
arguments = Bundle().apply {
putString(ARG_CURRENT_URL, currentUrl)
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/SelectImportPasswordMethodDialog.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/SelectImportPasswordMethodDialog.kt
new file mode 100644
index 000000000000..7ccc62ed23ac
--- /dev/null
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/SelectImportPasswordMethodDialog.kt
@@ -0,0 +1,279 @@
+/*
+ * Copyright (c) 2022 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.autofill.impl.ui.credential.management.viewing
+
+import android.app.Activity
+import android.content.Context
+import android.content.DialogInterface
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.IntentCompat
+import androidx.core.os.BundleCompat
+import androidx.fragment.app.setFragmentResult
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.duckduckgo.anvil.annotations.InjectWith
+import com.duckduckgo.app.browser.favicon.FaviconManager
+import com.duckduckgo.app.statistics.pixels.Pixel
+import com.duckduckgo.autofill.api.AutofillScreens.ImportGooglePassword
+import com.duckduckgo.autofill.impl.R
+import com.duckduckgo.autofill.impl.databinding.ContentChooseImportPasswordMethodBinding
+import com.duckduckgo.autofill.impl.deviceauth.AutofillAuthorizationGracePeriod
+import com.duckduckgo.autofill.impl.importing.PasswordImporter
+import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult
+import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult.Finished
+import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult
+import com.duckduckgo.autofill.impl.ui.credential.dialog.animateClosed
+import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ImportPasswordConfig
+import com.duckduckgo.autofill.impl.ui.credential.management.viewing.SelectImportPasswordMethodDialog.Companion.Result.UserChoseGcmImport
+import com.duckduckgo.di.scopes.FragmentScope
+import com.duckduckgo.navigation.api.GlobalActivityStarter
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import dagger.android.support.AndroidSupportInjection
+import javax.inject.Inject
+import kotlinx.coroutines.launch
+import kotlinx.parcelize.Parcelize
+import timber.log.Timber
+
+@InjectWith(FragmentScope::class)
+class SelectImportPasswordMethodDialog : BottomSheetDialogFragment() {
+
+ @Inject
+ lateinit var pixel: Pixel
+
+ @Inject
+ lateinit var passwordImporter: PasswordImporter
+
+ /**
+ * To capture all the ways the BottomSheet can be dismissed, we might end up with onCancel being called when we don't want it
+ * This flag is set to true when taking an action which dismisses the dialog, but should not be treated as a cancellation.
+ */
+ private var ignoreCancellationEvents = false
+
+ override fun getTheme(): Int = R.style.AutofillBottomSheetDialogTheme
+
+ @Inject
+ lateinit var faviconManager: FaviconManager
+
+ @Inject
+ lateinit var globalActivityStarter: GlobalActivityStarter
+
+ @Inject
+ lateinit var authorizationGracePeriod: AutofillAuthorizationGracePeriod
+
+ private var _binding: ContentChooseImportPasswordMethodBinding? = null
+
+ private val binding get() = _binding!!
+
+ private val importGooglePasswordsFlowLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
+ Timber.i("cdr onActivityResult for Google Password Manager import flow. resultCode=${activityResult.resultCode}")
+
+ if (activityResult.resultCode == Activity.RESULT_OK) {
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ activityResult.data?.let { data ->
+ val resultDetails = IntentCompat.getParcelableExtra(
+ data,
+ ImportGooglePasswordResult.RESULT_KEY_DETAILS,
+ ImportGooglePasswordResult::class.java,
+ )
+ when (resultDetails) {
+ is ImportGooglePasswordResult.Success -> {
+ binding.prePostViewSwitcher.displayedChild = 1
+ observeImportJob(resultDetails.importJobId)
+ }
+ is ImportGooglePasswordResult.Error -> processErrorResult()
+ is ImportGooglePasswordResult.UserCancelled, null -> {
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private suspend fun observeImportJob(jobId: String) {
+ passwordImporter.getImportStatus(jobId).collect {
+ when (it) {
+ is ImportResult.InProgress -> {
+ // we can show the in-progress state, update duplicates etc...
+ Timber.d("cdr Import in progress: ${it.savedCredentialIds.size} of ${it.importListSize}")
+ binding.postflow.inProgressFinishedViewSwitcher.displayedChild = 0
+ }
+
+ is Finished -> {
+ Timber.d(
+ "cdr Import finished: " +
+ "${it.savedCredentialIds.size} imported. " +
+ "${it.duplicatedPasswords.size} duplicates. " +
+ "Total=${it.importListSize}",
+ )
+ processSuccessResult(it)
+ }
+ }
+ }
+ }
+
+ private fun processSuccessResult(result: Finished) {
+ binding.postflow.importFinished.errorNotImported.visibility = GONE
+
+ with(binding.postflow.importFinished.resultsImported) {
+ setSecondaryText(result.savedCredentialIds.size.toString())
+ }
+
+ with(binding.postflow.importFinished.duplicatesNotImported) {
+ setSecondaryText(result.duplicatedPasswords.size.toString())
+ visibility = if (result.duplicatedPasswords.isNotEmpty()) VISIBLE else GONE
+ }
+
+ with(binding.postflow.importFinished.primaryCtaButton) {
+ setOnClickListener {
+ setResult(UserChoseGcmImport(result))
+ dismiss()
+ }
+ setText(R.string.importPasswordsProcessingResultDialogDoneButtonText)
+ }
+
+ binding.postflow.inProgressFinishedViewSwitcher.displayedChild = 1
+ }
+
+ private fun processErrorResult() {
+ binding.postflow.importFinished.resultsImported.visibility = GONE
+ binding.postflow.importFinished.duplicatesNotImported.visibility = GONE
+ binding.postflow.importFinished.errorNotImported.visibility = VISIBLE
+
+ with(binding.postflow.importFinished.primaryCtaButton) {
+ setOnClickListener {
+ launchImportGcmFlow()
+ }
+ text = getString(R.string.importPasswordsProcessingResultDialogRetryButtonText)
+ }
+
+ binding.prePostViewSwitcher.displayedChild = 1
+ }
+
+ override fun onAttach(context: Context) {
+ AndroidSupportInjection.inject(this)
+ super.onAttach(context)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (savedInstanceState != null) {
+ // If being created after a configuration change, dismiss the dialog as the WebView will be re-created too
+ dismiss()
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ _binding = ContentChooseImportPasswordMethodBinding.inflate(inflater, container, false)
+ configureViews(binding)
+ return binding.root
+ }
+
+ override fun onDestroyView() {
+ _binding = null
+ authorizationGracePeriod.removeRequestForExtendedGracePeriod()
+ super.onDestroyView()
+ }
+
+ private fun configureViews(binding: ContentChooseImportPasswordMethodBinding) {
+ (dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ configureCloseButton(binding)
+
+ val config =
+ BundleCompat.getParcelable(requireArguments(), INPUT_KEY_CONFIG, ImportPasswordConfig::class.java)!!
+
+ with(binding.preflow.importGcmButton) {
+ visibility = if (config.canImportFromGooglePasswordManager) VISIBLE else GONE
+ setOnClickListener { onImportGcmButtonClicked() }
+ }
+ }
+
+ private fun onImportGcmButtonClicked() {
+ launchImportGcmFlow()
+ }
+
+ private fun launchImportGcmFlow() {
+ authorizationGracePeriod.requestExtendedGracePeriod()
+
+ val intent = globalActivityStarter.startIntent(requireContext(), ImportGooglePassword.AutofillImportViaGooglePasswordManagerScreen)
+ importGooglePasswordsFlowLauncher.launch(intent)
+ }
+
+ private fun setResult(result: Result?) {
+ val resultBundle = Bundle().apply {
+ putParcelable(RESULT_KEY_DETAILS, result)
+ }
+ setFragmentResult(RESULT_KEY, resultBundle)
+ dismiss()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ if (ignoreCancellationEvents) {
+ Timber.v("onCancel: Ignoring cancellation event")
+ return
+ }
+ setResult(Result.UserCancelled)
+ }
+
+ private fun configureCloseButton(binding: ContentChooseImportPasswordMethodBinding) {
+ binding.closeButton.setOnClickListener { (dialog as BottomSheetDialog).animateClosed() }
+ }
+
+ companion object {
+
+ fun instance(configuration: ImportPasswordConfig): SelectImportPasswordMethodDialog {
+ val fragment = SelectImportPasswordMethodDialog()
+ fragment.arguments = Bundle().also {
+ it.putParcelable(INPUT_KEY_CONFIG, configuration)
+ }
+ return fragment
+ }
+
+ const val RESULT_KEY = "SelectImportPasswordMethodDialogResult"
+ const val RESULT_KEY_DETAILS = "SelectImportPasswordMethodDialogResultDetails"
+
+ private const val INPUT_KEY_CONFIG = "config"
+
+ sealed interface Result : Parcelable {
+
+ @Parcelize
+ data class UserChoseGcmImport(val importResult: ImportResult) : Result
+
+ @Parcelize
+ data object UserCancelled : Result
+
+ @Parcelize
+ data object ErrorDuringImport : Result
+ }
+ }
+}
diff --git a/autofill/autofill-impl/src/main/res/drawable/autofill_gpm_export_instruction.xml b/autofill/autofill-impl/src/main/res/drawable/autofill_gpm_export_instruction.xml
new file mode 100644
index 000000000000..6f7bde8abdef
--- /dev/null
+++ b/autofill/autofill-impl/src/main/res/drawable/autofill_gpm_export_instruction.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/autofill/autofill-impl/src/main/res/drawable/autofill_rounded_border_import_background.xml b/autofill/autofill-impl/src/main/res/drawable/autofill_rounded_border_import_background.xml
new file mode 100644
index 000000000000..b7a9c542829f
--- /dev/null
+++ b/autofill/autofill-impl/src/main/res/drawable/autofill_rounded_border_import_background.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/autofill/autofill-impl/src/main/res/drawable/ic_check_recolorable_24.xml b/autofill/autofill-impl/src/main/res/drawable/ic_check_recolorable_24.xml
new file mode 100644
index 000000000000..3e79b3d20fbe
--- /dev/null
+++ b/autofill/autofill-impl/src/main/res/drawable/ic_check_recolorable_24.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/autofill/autofill-impl/src/main/res/drawable/ic_cross_recolorable_red_24.xml b/autofill/autofill-impl/src/main/res/drawable/ic_cross_recolorable_red_24.xml
new file mode 100644
index 000000000000..432454a57240
--- /dev/null
+++ b/autofill/autofill-impl/src/main/res/drawable/ic_cross_recolorable_red_24.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/autofill/autofill-impl/src/main/res/drawable/ic_passwords_import_128.xml b/autofill/autofill-impl/src/main/res/drawable/ic_passwords_import_128.xml
new file mode 100644
index 000000000000..bcc09c00bf5e
--- /dev/null
+++ b/autofill/autofill-impl/src/main/res/drawable/ic_passwords_import_128.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml b/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml
index c234aed93668..d83cb014d0aa 100644
--- a/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml
+++ b/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml
@@ -72,7 +72,7 @@
android:text="@string/credentialManagementNoLoginsSavedSubtitle" />
+ android:text="@string/autofillImportGooglePasswordEmptyStateButtonTitle" />
+
+
diff --git a/autofill/autofill-impl/src/main/res/layout/content_choose_import_password_method.xml b/autofill/autofill-impl/src/main/res/layout/content_choose_import_password_method.xml
new file mode 100644
index 000000000000..3f43dec0cffd
--- /dev/null
+++ b/autofill/autofill-impl/src/main/res/layout/content_choose_import_password_method.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow.xml b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow.xml
new file mode 100644
index 000000000000..2ade5caa6f55
--- /dev/null
+++ b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_in_progress.xml b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_in_progress.xml
new file mode 100644
index 000000000000..ae4461bb485d
--- /dev/null
+++ b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_in_progress.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_result.xml b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_result.xml
new file mode 100644
index 000000000000..1e19e023fd1f
--- /dev/null
+++ b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_result.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/autofill/autofill-impl/src/main/res/layout/content_import_google_password_pre_flow.xml b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_pre_flow.xml
new file mode 100644
index 000000000000..d87170884123
--- /dev/null
+++ b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_pre_flow.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml b/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml
index d8b20f9b45df..ca4aecf0ee4d 100644
--- a/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml
+++ b/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml
@@ -40,15 +40,21 @@
app:showAsAction="never" />
+
+
\ No newline at end of file
diff --git a/autofill/autofill-impl/src/main/res/values/donottranslate.xml b/autofill/autofill-impl/src/main/res/values/donottranslate.xml
index eff5d24dd1e2..b028fb254d17 100644
--- a/autofill/autofill-impl/src/main/res/values/donottranslate.xml
+++ b/autofill/autofill-impl/src/main/res/values/donottranslate.xml
@@ -20,4 +20,24 @@
Import Google Passwords
%1$d passwords imported from Google
+ %1$d passwords imported from CSV
+
+ Import Passwords from Google
+ Import your Google Passwords
+
+ Sync Desktop Passwords
+ Sync Desktop Passwords
+
+ Import your Google Passwords
+ Google may require you to login or ask for your password to confirm.
+ Open Google Passwords
+ Choose a CSV file
+ Import from Desktop Browser
+
+ Import to DuckDuckGo
+ Got It
+ Retry
+ Password import failed
+ Duplicate Passwords Skipped
+ Passwords Imported
\ No newline at end of file
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 a33a0ad89d54..7c448398ab33 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
@@ -141,7 +141,7 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
observePasswordInputUpdates(jobId)
}
is CsvPasswordImportResult.Error -> {
- "Failed to import passwords due to an error".showSnackbar()
+ FAILED_IMPORT_GENERIC_ERROR.showSnackbar()
}
}
}
@@ -159,7 +159,7 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
observePasswordInputUpdates(resultDetails.importJobId)
}
Error -> {
- "Failed to import passwords due to an error".showSnackbar()
+ FAILED_IMPORT_GENERIC_ERROR.showSnackbar()
}
is UserCancelled, null -> {
}
@@ -581,6 +581,8 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
return Intent(context, AutofillInternalSettingsActivity::class.java)
}
+ private const val FAILED_IMPORT_GENERIC_ERROR = "Failed to import passwords due to an error"
+
private val sampleUrlList = listOf(
"fill.dev",
"duckduckgo.com",