Skip to content

Commit

Permalink
Launch import flow from Password management screen
Browse files Browse the repository at this point in the history
  • Loading branch information
CDRussell committed Nov 4, 2024
1 parent 920cf1e commit c02a62f
Show file tree
Hide file tree
Showing 24 changed files with 1,071 additions and 39 deletions.
1 change: 1 addition & 0 deletions autofill/autofill-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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:_"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
Expand All @@ -67,17 +86,35 @@ 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
}
}

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 = 180_000
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.databinding.FragmentImportGooglePasswordsWebflowBinding
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult.Companion.RESULT_KEY
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.Initializing
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.LoadStartPage
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.NavigatingBack
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.UserCancelledImportFlow
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.UserFinishedCannotImport
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.UserFinishedImportFlow
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.WebContentShowing
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowWebChromeClient.ProgressListener
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowWebViewClient.NewPageCallback
import com.duckduckgo.autofill.impl.importing.gpm.webflow.autofill.NoOpAutofillCallback
Expand Down Expand Up @@ -135,18 +138,19 @@ class ImportGooglePasswordsWebFlowFragment :
configureWebView()
configureBackButtonHandler()
observeViewState()
loadFirstWebpage()
viewModel.onViewCreated()
}

override fun onDestroyView() {
super.onDestroyView()
binding = null
}

private fun loadFirstWebpage() {
private fun loadFirstWebpage(url: String) {
lifecycleScope.launch(dispatchers.main()) {
autofillConfigurationJob.join()
binding?.webView?.loadUrl(STARTING_URL)
binding?.webView?.loadUrl(url)
viewModel.firstPageLoading()
}
}

Expand All @@ -157,8 +161,12 @@ class ImportGooglePasswordsWebFlowFragment :
when (viewState) {
is UserFinishedImportFlow -> exitFlowAsSuccess(viewState.bundle)
is UserCancelledImportFlow -> exitFlowAsCancellation(viewState.stage)
is UserFinishedCannotImport -> exitFlowAsImpossibleToImport(viewState.bundle)
is NavigatingBack -> binding?.webView?.goBack()
is Initializing -> {}
is LoadStartPage -> loadFirstWebpage(viewState.initialLaunchUrl)
is WebContentShowing, Initializing -> {
// no-op
}
}
}
}
Expand All @@ -173,6 +181,10 @@ class ImportGooglePasswordsWebFlowFragment :
setFragmentResult(RESULT_KEY, resultBundle)
}

private fun exitFlowAsImpossibleToImport(resultBundle: Bundle) {
setFragmentResult(RESULT_KEY, resultBundle)
}

private fun configureBackButtonHandler() {
activity?.let {
it.onBackPressedDispatcher.addCallback(
Expand Down Expand Up @@ -335,7 +347,6 @@ class ImportGooglePasswordsWebFlowFragment :
}

companion object {
private const val STARTING_URL = "https://passwords.google.com/options?ep=1"
private const val CUSTOM_FLOW_TAB_ID = "import-passwords-webflow"
private const val SELECT_CREDENTIALS_FRAGMENT_TAG = "autofillSelectCredentialsDialog"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@ package com.duckduckgo.autofill.impl.importing.gpm.webflow

import android.os.Bundle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.autofill.impl.importing.CsvPasswordConverter
import com.duckduckgo.autofill.impl.importing.CsvPasswordConverter.CsvPasswordImportResult
import com.duckduckgo.autofill.impl.importing.CsvPasswordConverter.CsvPasswordImportResult.Success
import com.duckduckgo.autofill.impl.importing.PasswordImporter
import com.duckduckgo.autofill.impl.importing.gpm.feature.AutofillImportPasswordConfigStore
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult.Companion.RESULT_KEY_DETAILS
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.Initializing
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber

@ContributesViewModel(ActivityScope::class)
Expand All @@ -44,9 +47,18 @@ class ImportGooglePasswordsWebFlowViewModel @Inject constructor() : ViewModel()
@Inject
lateinit var csvPasswordConverter: CsvPasswordConverter

@Inject
lateinit var autofillImportConfigStore: AutofillImportPasswordConfigStore

private val _viewState = MutableStateFlow<ViewState>(Initializing)
val viewState: StateFlow<ViewState> = _viewState

fun onViewCreated() {
viewModelScope.launch(dispatchers.io()) {
_viewState.value = ViewState.LoadStartPage(autofillImportConfigStore.getConfig().launchUrlGooglePasswords)
}
}

fun onPageStarted(url: String?) {
Timber.i("onPageStarted: $url")
}
Expand Down Expand Up @@ -91,7 +103,7 @@ class ImportGooglePasswordsWebFlowViewModel @Inject constructor() : ViewModel()
val resultBundle = Bundle().also {
it.putParcelable(RESULT_KEY_DETAILS, ImportGooglePasswordResult.Error)
}
_viewState.value = ViewState.UserFinishedImportFlow(resultBundle)
_viewState.value = ViewState.UserFinishedCannotImport(resultBundle)
} else {
terminateFlowAsCancellation(url ?: "unknown")
}
Expand All @@ -114,10 +126,17 @@ class ImportGooglePasswordsWebFlowViewModel @Inject constructor() : ViewModel()
_viewState.value = ViewState.UserCancelledImportFlow(stage)
}

fun firstPageLoading() {
_viewState.value = ViewState.WebContentShowing
}

sealed interface ViewState {
data object Initializing : ViewState
data object WebContentShowing : ViewState
data class LoadStartPage(val initialLaunchUrl: String) : ViewState
data class UserCancelledImportFlow(val stage: String) : ViewState
data class UserFinishedImportFlow(val bundle: Bundle) : ViewState
data class UserFinishedCannotImport(val bundle: Bundle) : ViewState
data object NavigatingBack : ViewState
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

package com.duckduckgo.autofill.impl.ui.credential.management

import android.os.Parcelable
import android.util.Patterns
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.app.browser.favicon.FaviconManager
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.autofill.api.AutofillFeature
import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource
import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.BrowserOverflow
import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.BrowserSnackbar
Expand All @@ -36,6 +38,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.AutofillImportPasswordConfigStore
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
Expand Down Expand Up @@ -89,6 +92,7 @@ import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsVie
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.LaunchResetNeverSaveListConfirmation
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.PromptUserToAuthenticateMassDeletion
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.ReevalutePromotions
import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordsManagementViewState
import com.duckduckgo.autofill.impl.ui.credential.management.neversaved.NeverSavedSitesViewState
import com.duckduckgo.autofill.impl.ui.credential.management.searching.CredentialListFilter
import com.duckduckgo.autofill.impl.ui.credential.management.viewing.duckaddress.DuckAddressIdentifier
Expand All @@ -112,6 +116,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)
Expand All @@ -133,6 +138,8 @@ class AutofillSettingsViewModel @Inject constructor(
private val autofillBreakageReportSender: AutofillBreakageReportSender,
private val autofillBreakageReportDataStore: AutofillSiteBreakageReportingDataStore,
private val autofillBreakageReportCanShowRules: AutofillBreakageReportCanShowRules,
private val autofillFeature: AutofillFeature,
private val importPasswordsConfigStore: AutofillImportPasswordConfigStore,
) : ViewModel() {

private val _viewState = MutableStateFlow(ViewState())
Expand All @@ -141,6 +148,9 @@ class AutofillSettingsViewModel @Inject constructor(
private val _neverSavedSitesViewState = MutableStateFlow(NeverSavedSitesViewState())
val neverSavedSitesViewState: StateFlow<NeverSavedSitesViewState> = _neverSavedSitesViewState

private val _importPasswordsViewState = MutableStateFlow(ImportPasswordsManagementViewState())
val importPasswordsViewState: StateFlow<ImportPasswordsManagementViewState> = _importPasswordsViewState

private val _commands = MutableStateFlow<List<Command>>(emptyList())
val commands: StateFlow<List<Command>> = _commands

Expand Down Expand Up @@ -431,6 +441,11 @@ class AutofillSettingsViewModel @Inject constructor(
_neverSavedSitesViewState.value = NeverSavedSitesViewState(showOptionToReset = count > 0)
}
}

viewModelScope.launch(dispatchers.io()) {
val canImportGooglePasswords = importPasswordsConfigStore.getConfig().canImportFromGooglePasswords
_importPasswordsViewState.value = ImportPasswordsManagementViewState(canImportGooglePasswords)
}
}

private suspend fun isBreakageReportingAllowed(): Boolean {
Expand Down Expand Up @@ -690,7 +705,13 @@ class AutofillSettingsViewModel @Inject constructor(
}

fun onImportPasswords() {
addCommand(LaunchImportPasswords)
viewModelScope.launch(dispatchers.io()) {
with(autofillFeature) {
val gpmImport = self().isEnabled() && canImportFromGooglePasswordManager().isEnabled()
val importConfig = ImportPasswordConfig(canImportFromGooglePasswordManager = gpmImport)
addCommand(LaunchImportPasswords(importConfig))
}
}
}

fun onReportBreakageClicked() {
Expand All @@ -702,7 +723,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,
Expand Down Expand Up @@ -854,12 +878,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()
Expand Down
Loading

0 comments on commit c02a62f

Please sign in to comment.