Skip to content

Commit

Permalink
Add developer settings for importing passwords from Google Password M…
Browse files Browse the repository at this point in the history
…anager
  • Loading branch information
CDRussell committed Oct 21, 2024
1 parent 7513429 commit 91e1c29
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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

import android.os.Parcelable
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult
import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult.Finished
import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult.InProgress
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize

interface PasswordImporter {
suspend fun importPasswords(importList: List<LoginCredentials>)
fun getImportStatus(): Flow<ImportResult>

sealed interface ImportResult : Parcelable {

@Parcelize
data class InProgress(
val savedCredentialIds: List<Long>,
val duplicatedPasswords: List<LoginCredentials>,
val importListSize: Int,
) : ImportResult

@Parcelize
data class Finished(
val savedCredentialIds: List<Long>,
val duplicatedPasswords: List<LoginCredentials>,
val importListSize: Int,
) : ImportResult
}
}

@SingleInstanceIn(AppScope::class)
@ContributesBinding(AppScope::class)
class PasswordImporterImpl @Inject constructor(
private val existingPasswordMatchDetector: ExistingPasswordMatchDetector,
private val autofillStore: InternalAutofillStore,
private val dispatchers: DispatcherProvider,
) : PasswordImporter {

private val _importStatus = MutableSharedFlow<ImportResult>(replay = 1)

override suspend fun importPasswords(importList: List<LoginCredentials>) {
return withContext(dispatchers.io()) {
val savedCredentialIds = mutableListOf<Long>()
val duplicatedPasswords = mutableListOf<LoginCredentials>()

importList.forEach {
if (!existingPasswordMatchDetector.alreadyExists(it)) {
val insertedId = autofillStore.saveCredentials(it.domain!!, it)?.id

if (insertedId != null) {
savedCredentialIds.add(insertedId)
}
} else {
duplicatedPasswords.add(it)
}

_importStatus.emit(InProgress(savedCredentialIds, duplicatedPasswords, importList.size))
}

_importStatus.emit(Finished(savedCredentialIds, duplicatedPasswords, importList.size))
}
}

override fun getImportStatus(): Flow<ImportResult> {
return _importStatus
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@

package com.duckduckgo.autofill.internal

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Lifecycle.State.STARTED
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.tabs.BrowserNav
import com.duckduckgo.autofill.api.AutofillFeature
import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreen
import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.InternalDevSettings
Expand All @@ -33,6 +37,11 @@ import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.impl.configuration.AutofillJavascriptEnvironmentConfiguration
import com.duckduckgo.autofill.impl.email.incontext.store.EmailProtectionInContextDataStore
import com.duckduckgo.autofill.impl.engagement.store.AutofillEngagementRepository
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter.ParseResult
import com.duckduckgo.autofill.impl.importing.PasswordImporter
import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult.Finished
import com.duckduckgo.autofill.impl.importing.PasswordImporter.ImportResult.InProgress
import com.duckduckgo.autofill.impl.reporting.AutofillSiteBreakageReportingDataStore
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository
Expand All @@ -48,6 +57,7 @@ import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.navigation.api.GlobalActivityStarter
import com.google.android.material.snackbar.Snackbar
import java.text.SimpleDateFormat
import javax.inject.Inject
import kotlinx.coroutines.flow.first
Expand Down Expand Up @@ -75,6 +85,12 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
@Inject
lateinit var autofillStore: InternalAutofillStore

@Inject
lateinit var passwordImporter: PasswordImporter

@Inject
lateinit var browserNav: BrowserNav

@Inject
lateinit var autofillPrefsStore: AutofillPrefsStore

Expand All @@ -101,6 +117,50 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
@Inject
lateinit var reportBreakageDataStore: AutofillSiteBreakageReportingDataStore

@Inject
lateinit var csvPasswordImporter: CsvPasswordImporter

private val importCsvLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
val fileUrl = data?.data

logcat { "cdr onActivityResult for CSV file request. resultCode=${result.resultCode}. uri=$fileUrl" }
if (fileUrl != null) {
lifecycleScope.launch {
when (val parseResult = csvPasswordImporter.readCsv(fileUrl)) {
is ParseResult.Success -> {
observePasswordInputUpdates()
passwordImporter.importPasswords(parseResult.loginCredentialsToImport)
}
is ParseResult.Error -> {
"Failed to import passwords due to an error".showSnackbar()
}
}
}
}
}
}

private fun observePasswordInputUpdates() {
lifecycleScope.launch {
repeatOnLifecycle(STARTED) {
passwordImporter.getImportStatus().collect {
when (it) {
is InProgress -> {
logcat { "cdr import status: $it" }
}

is Finished -> {
logcat { "cdr Imported ${it.savedCredentialIds.size} passwords" }
"Imported ${it.savedCredentialIds.size} passwords".showSnackbar()
}
}
}
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
Expand Down Expand Up @@ -168,6 +228,7 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
configureEngagementEventHandlers()
configureReportBreakagesHandlers()
configureDeclineCounterHandlers()
configureImportPasswordsEventHandlers()
}

private fun configureReportBreakagesHandlers() {
Expand All @@ -179,6 +240,22 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
}
}

@SuppressLint("QueryPermissionsNeeded")
private fun configureImportPasswordsEventHandlers() {
binding.importPasswordsLaunchGooglePasswordWebpage.setClickListener {
val googlePasswordsUrl = "https://passwords.google.com/options?ep=1"
startActivity(browserNav.openInNewTab(this, googlePasswordsUrl))
}

binding.importPasswordsImportCsv.setClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
importCsvLauncher.launch(intent)
}
}

private fun configureEngagementEventHandlers() {
binding.engagementClearEngagementHistoryButton.setOnClickListener {
lifecycleScope.launch(dispatchers.io()) {
Expand Down Expand Up @@ -443,6 +520,10 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() {
}
}

private fun String.showSnackbar(duration: Int = Snackbar.LENGTH_LONG) {
Snackbar.make(binding.root, this, duration).show()
}

private fun Context.daysInstalledOverrideOptions(): List<Pair<String, Int>> {
return listOf(
Pair(getString(R.string.autofillDevSettingsOverrideMaxInstalledOptionNever), -1),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,27 @@
android:layout_height="wrap_content"
app:primaryText="@string/autofillDevSettingsViewSavedLogins" />

<com.duckduckgo.common.ui.view.listitem.SectionHeaderListItem
android:id="@+id/importPasswordsSectionTitle"
android:layout_width="match_parent"
android:layout_marginTop="20dp"
android:layout_height="wrap_content"
app:primaryText="@string/autofillDevSettingsImportPasswordsTitle" />


<com.duckduckgo.common.ui.view.listitem.OneLineListItem
android:id="@+id/importPasswordsLaunchGooglePasswordWebpage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:primaryText="@string/autofillDevSettingsImportPasswordsExportPasswordsOurAppTitle" />

<com.duckduckgo.common.ui.view.listitem.OneLineListItem
android:id="@+id/importPasswordsImportCsv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:primaryText="@string/autofillDevSettingsImportPasswordsImportPasswordsCsvTitle" />


<com.duckduckgo.common.ui.view.divider.HorizontalDivider
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
<string name="autofillDevSettingsNeverSavedSitesCountSubtitle" instruction="%1$d is count of how many websites have been added to a list">Number of sites: %1$d</string>
<string name="autofillDevSettingsNeverSavedSitesAddSampleButton">Add sample site (fill.dev)</string>

<string name="autofillDevSettingsImportPasswordsTitle">Import Passwords</string>
<string name="autofillDevSettingsImportPasswordsExportPasswordsOurAppTitle">Launch Google Passwords (normal tab)</string>
<string name="autofillDevSettingsImportPasswordsImportPasswordsCsvTitle">Import CSV</string>

<string name="autofillDevSettingsOverrideMaxInstallDialogTitle">Maximum number of days since install</string>
<string name="autofillDevSettingsOverrideMaxInstallDialogOkButtonText">OK</string>
<string name="autofillDevSettingsOverrideMaxInstallDialogCancelButtonText">Cancel</string>
Expand Down

0 comments on commit 91e1c29

Please sign in to comment.