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 Oct 22, 2024
1 parent d88c237 commit 521f408
Show file tree
Hide file tree
Showing 20 changed files with 898 additions and 63 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,11 @@ interface AutofillAuthorizationGracePeriod {
*/
fun recordSuccessfulAuthorization()

/**
* Requests an extended grace period. This may extend the grace period to a longer duration.
*/
fun requestExtendedGracePeriod()

/**
* Invalidates the grace period, so that the next call to [isAuthRequired] will return true
*/
Expand All @@ -53,12 +58,17 @@ 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 isAuthRequired(): Boolean {
lastSuccessfulAuthTime?.let { lastAuthTime ->
val timeSinceLastAuth = timeProvider.currentTimeMillis() - lastAuthTime
Expand All @@ -67,17 +77,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
}
}

extendedGraceTimeRequested = null
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
extendedGraceTimeRequested = null
}

companion object {
private const val AUTH_GRACE_PERIOD_MS = 15_000
private const val AUTH_GRACE_EXTENDED_PERIOD_MS = 60_000
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2024 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.importing.gpm.webflow

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

sealed interface ImportGooglePasswordResult : Parcelable {

@Parcelize
data class Success(val importedCount: Int, val foundInImport: Int, val importJobId: String) : ImportGooglePasswordResult

@Parcelize
data class UserCancelled(val stage: String) : ImportGooglePasswordResult

@Parcelize
data object Error : ImportGooglePasswordResult

companion object {
const val RESULT_KEY = "importResult"
const val RESULT_KEY_DETAILS = "importResultDetails"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.autofill.api.AutofillScreens.ImportGooglePassword.AutofillImportViaGooglePasswordManagerScreen
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.databinding.ActivityImportGooglePasswordsWebflowBinding
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowFragment.Companion.Result.Companion.RESULT_KEY
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowFragment.Companion.Result.Companion.RESULT_KEY_DETAILS
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowFragment.Companion.Result.UserCancelled
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult.Companion.RESULT_KEY
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult.Companion.RESULT_KEY_DETAILS
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult.UserCancelled
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.di.scopes.ActivityScope
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package com.duckduckgo.autofill.impl.importing.gpm.webflow
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.webkit.WebSettings
import android.webkit.WebView
Expand All @@ -46,8 +45,6 @@ import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter.ParseResult.Error
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter.ParseResult.Success
import com.duckduckgo.autofill.impl.importing.PasswordImporter
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowFragment.Companion.Result.Companion.RESULT_KEY
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowFragment.Companion.Result.Companion.RESULT_KEY_DETAILS
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.NavigatingBack
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.ShowingWebContent
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.UserCancelledImportFlow
Expand All @@ -57,6 +54,8 @@ import com.duckduckgo.autofill.impl.importing.gpm.webflow.autofill.NoOpAutofillC
import com.duckduckgo.autofill.impl.importing.gpm.webflow.autofill.NoOpAutofillEventListener
import com.duckduckgo.autofill.impl.importing.gpm.webflow.autofill.NoOpEmailProtectionInContextSignupFlowListener
import com.duckduckgo.autofill.impl.importing.gpm.webflow.autofill.NoOpEmailProtectionUserPromptListener
import com.duckduckgo.autofill.impl.ui.credential.management.viewing.SelectImportPasswordMethodDialog.Companion.RESULT_KEY
import com.duckduckgo.autofill.impl.ui.credential.management.viewing.SelectImportPasswordMethodDialog.Companion.RESULT_KEY_DETAILS
import com.duckduckgo.common.ui.DuckDuckGoFragment
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.common.utils.ConflatedJob
Expand All @@ -68,7 +67,6 @@ import com.duckduckgo.user.agent.api.UserAgentProvider
import javax.inject.Inject
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import timber.log.Timber

@InjectWith(FragmentScope::class)
Expand Down Expand Up @@ -308,20 +306,15 @@ class ImportGooglePasswordsWebFlowFragment :

override suspend fun onCsvAvailable(csv: String) {
Timber.i("cdr CSV available %s", csv)
val parseResult = csvPasswordImporter.readCsv(csv)
when (parseResult) {
when (val parseResult = csvPasswordImporter.readCsv(csv)) {
is Success -> {
passwordImporter.importPasswords(parseResult.loginCredentialsToImport)
onCsvParsed(parseResult)
}

Error -> {
Timber.e("cdr Error parsing CSV")
is Error -> {
onCsvError()
}
}

val resultBundle = Bundle().also { it.putParcelable(RESULT_KEY_DETAILS, parseResult) }
setFragmentResult(RESULT_KEY, resultBundle)

/**
* val result = csvPasswordImporter.importCsv(csv)
* val resultDetails = when (result) {
Expand All @@ -337,10 +330,25 @@ class ImportGooglePasswordsWebFlowFragment :
*/
}

private suspend fun onCsvParsed(parseResult: Success) {
val jobId = passwordImporter.importPasswords(parseResult.loginCredentialsToImport)
val resultBundle = Bundle().also {
it.putParcelable(
RESULT_KEY_DETAILS,
ImportGooglePasswordResult.Success(
importedCount = parseResult.loginCredentialsToImport.size,
foundInImport = parseResult.loginCredentialsToImport.size,
importJobId = jobId,
),
)
}
setFragmentResult(RESULT_KEY, resultBundle)
}

override suspend fun onCsvError() {
Timber.e("cdr Error decoding CSV")
val resultBundle = Bundle().also {
it.putParcelable(RESULT_KEY_DETAILS, Result.Error)
it.putParcelable(RESULT_KEY_DETAILS, ImportGooglePasswordResult.Error)
}
setFragmentResult(RESULT_KEY, resultBundle)
}
Expand Down Expand Up @@ -368,22 +376,5 @@ class ImportGooglePasswordsWebFlowFragment :
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"

sealed interface Result : Parcelable {

companion object {
const val RESULT_KEY = "importResult"
const val RESULT_KEY_DETAILS = "importResultDetails"
}

@Parcelize
data class Success(val importedCount: Int, val foundInImport: Int) : Result

@Parcelize
data class UserCancelled(val stage: String) : Result

@Parcelize
data object Error : Result
}
}
}
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,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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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())
Expand Down Expand Up @@ -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() {
Expand All @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
Loading

0 comments on commit 521f408

Please sign in to comment.